Create the forntend API definitions for recipe service
This commit is contained in:
478
frontend/src/api/hooks/recipes.ts
Normal file
478
frontend/src/api/hooks/recipes.ts
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
/**
|
||||||
|
* Recipes React Query hooks
|
||||||
|
* Data fetching and caching layer for recipe management
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
useQueryClient,
|
||||||
|
UseQueryOptions,
|
||||||
|
UseMutationOptions,
|
||||||
|
useInfiniteQuery,
|
||||||
|
UseInfiniteQueryOptions
|
||||||
|
} from '@tanstack/react-query';
|
||||||
|
import { recipesService } from '../services/recipes';
|
||||||
|
import { ApiError } from '../client/apiClient';
|
||||||
|
import type {
|
||||||
|
RecipeResponse,
|
||||||
|
RecipeCreate,
|
||||||
|
RecipeUpdate,
|
||||||
|
RecipeSearchParams,
|
||||||
|
RecipeDuplicateRequest,
|
||||||
|
RecipeFeasibilityResponse,
|
||||||
|
RecipeStatisticsResponse,
|
||||||
|
RecipeCategoriesResponse,
|
||||||
|
ProductionBatchResponse,
|
||||||
|
ProductionBatchCreate,
|
||||||
|
ProductionBatchUpdate,
|
||||||
|
} from '../types/recipes';
|
||||||
|
|
||||||
|
// Query Keys Factory
|
||||||
|
export const recipesKeys = {
|
||||||
|
all: ['recipes'] as const,
|
||||||
|
lists: () => [...recipesKeys.all, 'list'] as const,
|
||||||
|
list: (filters: RecipeSearchParams) => [...recipesKeys.lists(), { filters }] as const,
|
||||||
|
details: () => [...recipesKeys.all, 'detail'] as const,
|
||||||
|
detail: (id: string) => [...recipesKeys.details(), id] as const,
|
||||||
|
statistics: () => [...recipesKeys.all, 'statistics'] as const,
|
||||||
|
categories: () => [...recipesKeys.all, 'categories'] as const,
|
||||||
|
feasibility: (id: string, batchMultiplier: number) => [...recipesKeys.all, 'feasibility', id, batchMultiplier] as const,
|
||||||
|
|
||||||
|
// Production batch keys
|
||||||
|
productionBatches: {
|
||||||
|
all: ['production-batches'] as const,
|
||||||
|
lists: () => [...recipesKeys.productionBatches.all, 'list'] as const,
|
||||||
|
list: (filters: any) => [...recipesKeys.productionBatches.lists(), { filters }] as const,
|
||||||
|
details: () => [...recipesKeys.productionBatches.all, 'detail'] as const,
|
||||||
|
detail: (id: string) => [...recipesKeys.productionBatches.details(), id] as const,
|
||||||
|
byRecipe: (recipeId: string) => [...recipesKeys.productionBatches.all, 'recipe', recipeId] as const,
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Recipe Queries
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single recipe by ID
|
||||||
|
*/
|
||||||
|
export const useRecipe = (
|
||||||
|
recipeId: string,
|
||||||
|
options?: Omit<UseQueryOptions<RecipeResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||||
|
) => {
|
||||||
|
return useQuery<RecipeResponse, ApiError>({
|
||||||
|
queryKey: recipesKeys.detail(recipeId),
|
||||||
|
queryFn: () => recipesService.getRecipe(recipeId),
|
||||||
|
enabled: !!recipeId,
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search/list recipes with filters
|
||||||
|
*/
|
||||||
|
export const useRecipes = (
|
||||||
|
filters: RecipeSearchParams = {},
|
||||||
|
options?: Omit<UseQueryOptions<RecipeResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||||
|
) => {
|
||||||
|
return useQuery<RecipeResponse[], ApiError>({
|
||||||
|
queryKey: recipesKeys.list(filters),
|
||||||
|
queryFn: () => recipesService.searchRecipes(filters),
|
||||||
|
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infinite query for recipes (pagination)
|
||||||
|
*/
|
||||||
|
export const useInfiniteRecipes = (
|
||||||
|
filters: Omit<RecipeSearchParams, 'offset'> = {},
|
||||||
|
options?: Omit<UseInfiniteQueryOptions<RecipeResponse[], ApiError>, 'queryKey' | 'queryFn' | 'getNextPageParam'>
|
||||||
|
) => {
|
||||||
|
return useInfiniteQuery<RecipeResponse[], ApiError>({
|
||||||
|
queryKey: recipesKeys.list(filters),
|
||||||
|
queryFn: ({ pageParam = 0 }) =>
|
||||||
|
recipesService.searchRecipes({ ...filters, offset: pageParam }),
|
||||||
|
getNextPageParam: (lastPage, allPages) => {
|
||||||
|
const limit = filters.limit || 100;
|
||||||
|
if (lastPage.length < limit) return undefined;
|
||||||
|
return allPages.length * limit;
|
||||||
|
},
|
||||||
|
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recipe statistics
|
||||||
|
*/
|
||||||
|
export const useRecipeStatistics = (
|
||||||
|
options?: Omit<UseQueryOptions<RecipeStatisticsResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||||
|
) => {
|
||||||
|
return useQuery<RecipeStatisticsResponse, ApiError>({
|
||||||
|
queryKey: recipesKeys.statistics(),
|
||||||
|
queryFn: () => recipesService.getRecipeStatistics(),
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recipe categories
|
||||||
|
*/
|
||||||
|
export const useRecipeCategories = (
|
||||||
|
options?: Omit<UseQueryOptions<RecipeCategoriesResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||||
|
) => {
|
||||||
|
return useQuery<RecipeCategoriesResponse, ApiError>({
|
||||||
|
queryKey: recipesKeys.categories(),
|
||||||
|
queryFn: () => recipesService.getRecipeCategories(),
|
||||||
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check recipe feasibility
|
||||||
|
*/
|
||||||
|
export const useRecipeFeasibility = (
|
||||||
|
recipeId: string,
|
||||||
|
batchMultiplier: number = 1.0,
|
||||||
|
options?: Omit<UseQueryOptions<RecipeFeasibilityResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||||
|
) => {
|
||||||
|
return useQuery<RecipeFeasibilityResponse, ApiError>({
|
||||||
|
queryKey: recipesKeys.feasibility(recipeId, batchMultiplier),
|
||||||
|
queryFn: () => recipesService.checkRecipeFeasibility(recipeId, batchMultiplier),
|
||||||
|
enabled: !!recipeId,
|
||||||
|
staleTime: 1 * 60 * 1000, // 1 minute (fresher data for inventory checks)
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recipe Mutations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new recipe
|
||||||
|
*/
|
||||||
|
export const useCreateRecipe = (
|
||||||
|
options?: UseMutationOptions<RecipeResponse, ApiError, RecipeCreate>
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<RecipeResponse, ApiError, RecipeCreate>({
|
||||||
|
mutationFn: (recipeData: RecipeCreate) => recipesService.createRecipe(recipeData),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
// Add to lists cache
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.lists() });
|
||||||
|
// Set individual recipe cache
|
||||||
|
queryClient.setQueryData(recipesKeys.detail(data.id), data);
|
||||||
|
// Invalidate statistics
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.statistics() });
|
||||||
|
// Invalidate categories (new category might be added)
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.categories() });
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing recipe
|
||||||
|
*/
|
||||||
|
export const useUpdateRecipe = (
|
||||||
|
options?: UseMutationOptions<RecipeResponse, ApiError, { id: string; data: RecipeUpdate }>
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<RecipeResponse, ApiError, { id: string; data: RecipeUpdate }>({
|
||||||
|
mutationFn: ({ id, data }) => recipesService.updateRecipe(id, data),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
// Update individual recipe cache
|
||||||
|
queryClient.setQueryData(recipesKeys.detail(data.id), data);
|
||||||
|
// Invalidate lists (recipe might move in search results)
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.lists() });
|
||||||
|
// Invalidate statistics
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.statistics() });
|
||||||
|
// Invalidate categories
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.categories() });
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a recipe
|
||||||
|
*/
|
||||||
|
export const useDeleteRecipe = (
|
||||||
|
options?: UseMutationOptions<{ message: string }, ApiError, string>
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{ message: string }, ApiError, string>({
|
||||||
|
mutationFn: (recipeId: string) => recipesService.deleteRecipe(recipeId),
|
||||||
|
onSuccess: (_, recipeId) => {
|
||||||
|
// Remove from individual cache
|
||||||
|
queryClient.removeQueries({ queryKey: recipesKeys.detail(recipeId) });
|
||||||
|
// Invalidate lists
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.lists() });
|
||||||
|
// Invalidate statistics
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.statistics() });
|
||||||
|
// Invalidate categories
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.categories() });
|
||||||
|
// Invalidate production batches for this recipe
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.productionBatches.byRecipe(recipeId) });
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate a recipe
|
||||||
|
*/
|
||||||
|
export const useDuplicateRecipe = (
|
||||||
|
options?: UseMutationOptions<RecipeResponse, ApiError, { id: string; data: RecipeDuplicateRequest }>
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<RecipeResponse, ApiError, { id: string; data: RecipeDuplicateRequest }>({
|
||||||
|
mutationFn: ({ id, data }) => recipesService.duplicateRecipe(id, data),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
// Add to lists cache
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.lists() });
|
||||||
|
// Set individual recipe cache
|
||||||
|
queryClient.setQueryData(recipesKeys.detail(data.id), data);
|
||||||
|
// Invalidate statistics
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.statistics() });
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate a recipe
|
||||||
|
*/
|
||||||
|
export const useActivateRecipe = (
|
||||||
|
options?: UseMutationOptions<RecipeResponse, ApiError, string>
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<RecipeResponse, ApiError, string>({
|
||||||
|
mutationFn: (recipeId: string) => recipesService.activateRecipe(recipeId),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
// Update individual recipe cache
|
||||||
|
queryClient.setQueryData(recipesKeys.detail(data.id), data);
|
||||||
|
// Invalidate lists
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.lists() });
|
||||||
|
// Invalidate statistics
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.statistics() });
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Production Batch Queries
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get production batch by ID (recipe-specific)
|
||||||
|
*/
|
||||||
|
export const useRecipeProductionBatch = (
|
||||||
|
batchId: string,
|
||||||
|
options?: Omit<UseQueryOptions<ProductionBatchResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||||
|
) => {
|
||||||
|
return useQuery<ProductionBatchResponse, ApiError>({
|
||||||
|
queryKey: recipesKeys.productionBatches.detail(batchId),
|
||||||
|
queryFn: () => recipesService.getProductionBatch(batchId),
|
||||||
|
enabled: !!batchId,
|
||||||
|
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get production batches with filters (recipe-specific)
|
||||||
|
*/
|
||||||
|
export const useRecipeProductionBatches = (
|
||||||
|
filters: {
|
||||||
|
recipe_id?: string;
|
||||||
|
status?: string;
|
||||||
|
start_date?: string;
|
||||||
|
end_date?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
} = {},
|
||||||
|
options?: Omit<UseQueryOptions<ProductionBatchResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||||
|
) => {
|
||||||
|
return useQuery<ProductionBatchResponse[], ApiError>({
|
||||||
|
queryKey: recipesKeys.productionBatches.list(filters),
|
||||||
|
queryFn: () => recipesService.getProductionBatches(filters),
|
||||||
|
staleTime: 1 * 60 * 1000, // 1 minute
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get production batches for a specific recipe
|
||||||
|
*/
|
||||||
|
export const useRecipeProductionBatchesByRecipe = (
|
||||||
|
recipeId: string,
|
||||||
|
options?: Omit<UseQueryOptions<ProductionBatchResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||||
|
) => {
|
||||||
|
return useQuery<ProductionBatchResponse[], ApiError>({
|
||||||
|
queryKey: recipesKeys.productionBatches.byRecipe(recipeId),
|
||||||
|
queryFn: () => recipesService.getRecipeProductionBatches(recipeId),
|
||||||
|
enabled: !!recipeId,
|
||||||
|
staleTime: 1 * 60 * 1000, // 1 minute
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Production Batch Mutations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a production batch for recipe
|
||||||
|
*/
|
||||||
|
export const useCreateRecipeProductionBatch = (
|
||||||
|
options?: UseMutationOptions<ProductionBatchResponse, ApiError, ProductionBatchCreate>
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<ProductionBatchResponse, ApiError, ProductionBatchCreate>({
|
||||||
|
mutationFn: (batchData: ProductionBatchCreate) => recipesService.createProductionBatch(batchData),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
// Set individual batch cache
|
||||||
|
queryClient.setQueryData(recipesKeys.productionBatches.detail(data.id), data);
|
||||||
|
// Invalidate batch lists
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.productionBatches.lists() });
|
||||||
|
// Invalidate recipe-specific batches
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.productionBatches.byRecipe(data.recipe_id) });
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a production batch for recipe
|
||||||
|
*/
|
||||||
|
export const useUpdateRecipeProductionBatch = (
|
||||||
|
options?: UseMutationOptions<ProductionBatchResponse, ApiError, { id: string; data: ProductionBatchUpdate }>
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<ProductionBatchResponse, ApiError, { id: string; data: ProductionBatchUpdate }>({
|
||||||
|
mutationFn: ({ id, data }) => recipesService.updateProductionBatch(id, data),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
// Update individual batch cache
|
||||||
|
queryClient.setQueryData(recipesKeys.productionBatches.detail(data.id), data);
|
||||||
|
// Invalidate batch lists
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.productionBatches.lists() });
|
||||||
|
// Invalidate recipe-specific batches
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.productionBatches.byRecipe(data.recipe_id) });
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a production batch for recipe
|
||||||
|
*/
|
||||||
|
export const useDeleteRecipeProductionBatch = (
|
||||||
|
options?: UseMutationOptions<{ message: string }, ApiError, string>
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{ message: string }, ApiError, string>({
|
||||||
|
mutationFn: (batchId: string) => recipesService.deleteProductionBatch(batchId),
|
||||||
|
onSuccess: (_, batchId) => {
|
||||||
|
// Remove from individual cache
|
||||||
|
queryClient.removeQueries({ queryKey: recipesKeys.productionBatches.detail(batchId) });
|
||||||
|
// Invalidate batch lists
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.productionBatches.lists() });
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a production batch for recipe
|
||||||
|
*/
|
||||||
|
export const useStartRecipeProductionBatch = (
|
||||||
|
options?: UseMutationOptions<ProductionBatchResponse, ApiError, string>
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<ProductionBatchResponse, ApiError, string>({
|
||||||
|
mutationFn: (batchId: string) => recipesService.startProductionBatch(batchId),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
// Update individual batch cache
|
||||||
|
queryClient.setQueryData(recipesKeys.productionBatches.detail(data.id), data);
|
||||||
|
// Invalidate batch lists
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.productionBatches.lists() });
|
||||||
|
// Invalidate recipe-specific batches
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.productionBatches.byRecipe(data.recipe_id) });
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete a production batch for recipe
|
||||||
|
*/
|
||||||
|
export const useCompleteRecipeProductionBatch = (
|
||||||
|
options?: UseMutationOptions<ProductionBatchResponse, ApiError, {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
actual_quantity?: number;
|
||||||
|
quality_score?: number;
|
||||||
|
quality_notes?: string;
|
||||||
|
waste_quantity?: number;
|
||||||
|
waste_reason?: string;
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<ProductionBatchResponse, ApiError, {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
actual_quantity?: number;
|
||||||
|
quality_score?: number;
|
||||||
|
quality_notes?: string;
|
||||||
|
waste_quantity?: number;
|
||||||
|
waste_reason?: string;
|
||||||
|
}
|
||||||
|
}>({
|
||||||
|
mutationFn: ({ id, data }) => recipesService.completeProductionBatch(id, data),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
// Update individual batch cache
|
||||||
|
queryClient.setQueryData(recipesKeys.productionBatches.detail(data.id), data);
|
||||||
|
// Invalidate batch lists
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.productionBatches.lists() });
|
||||||
|
// Invalidate recipe-specific batches
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.productionBatches.byRecipe(data.recipe_id) });
|
||||||
|
// Invalidate inventory queries (production affects inventory)
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['inventory'] });
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a production batch for recipe
|
||||||
|
*/
|
||||||
|
export const useCancelRecipeProductionBatch = (
|
||||||
|
options?: UseMutationOptions<ProductionBatchResponse, ApiError, { id: string; reason?: string }>
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<ProductionBatchResponse, ApiError, { id: string; reason?: string }>({
|
||||||
|
mutationFn: ({ id, reason }) => recipesService.cancelProductionBatch(id, reason),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
// Update individual batch cache
|
||||||
|
queryClient.setQueryData(recipesKeys.productionBatches.detail(data.id), data);
|
||||||
|
// Invalidate batch lists
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.productionBatches.lists() });
|
||||||
|
// Invalidate recipe-specific batches
|
||||||
|
queryClient.invalidateQueries({ queryKey: recipesKeys.productionBatches.byRecipe(data.recipe_id) });
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -28,6 +28,7 @@ export { OrdersService } from './services/orders';
|
|||||||
export { forecastingService } from './services/forecasting';
|
export { forecastingService } from './services/forecasting';
|
||||||
export { productionService } from './services/production';
|
export { productionService } from './services/production';
|
||||||
export { posService } from './services/pos';
|
export { posService } from './services/pos';
|
||||||
|
export { recipesService } from './services/recipes';
|
||||||
|
|
||||||
// Types - Auth
|
// Types - Auth
|
||||||
export type {
|
export type {
|
||||||
@@ -374,6 +375,31 @@ export type {
|
|||||||
POSEnvironment,
|
POSEnvironment,
|
||||||
} from './types/pos';
|
} from './types/pos';
|
||||||
|
|
||||||
|
// Types - Recipes
|
||||||
|
export type {
|
||||||
|
RecipeStatus,
|
||||||
|
MeasurementUnit,
|
||||||
|
ProductionStatus as RecipeProductionStatus,
|
||||||
|
ProductionPriority as RecipeProductionPriority,
|
||||||
|
RecipeIngredientCreate,
|
||||||
|
RecipeIngredientUpdate,
|
||||||
|
RecipeIngredientResponse,
|
||||||
|
RecipeCreate,
|
||||||
|
RecipeUpdate,
|
||||||
|
RecipeResponse,
|
||||||
|
RecipeSearchRequest,
|
||||||
|
RecipeSearchParams,
|
||||||
|
RecipeDuplicateRequest,
|
||||||
|
RecipeFeasibilityResponse,
|
||||||
|
RecipeStatisticsResponse,
|
||||||
|
RecipeCategoriesResponse,
|
||||||
|
ProductionBatchCreate as RecipeProductionBatchCreate,
|
||||||
|
ProductionBatchUpdate as RecipeProductionBatchUpdate,
|
||||||
|
ProductionBatchResponse as RecipeProductionBatchResponse,
|
||||||
|
RecipeFormData,
|
||||||
|
RecipeUpdateFormData,
|
||||||
|
} from './types/recipes';
|
||||||
|
|
||||||
// Hooks - Auth
|
// Hooks - Auth
|
||||||
export {
|
export {
|
||||||
useAuthProfile,
|
useAuthProfile,
|
||||||
@@ -689,12 +715,37 @@ export {
|
|||||||
posKeys,
|
posKeys,
|
||||||
} from './hooks/pos';
|
} from './hooks/pos';
|
||||||
|
|
||||||
|
// Hooks - Recipes
|
||||||
|
export {
|
||||||
|
useRecipe,
|
||||||
|
useRecipes,
|
||||||
|
useInfiniteRecipes,
|
||||||
|
useRecipeStatistics,
|
||||||
|
useRecipeCategories,
|
||||||
|
useRecipeFeasibility,
|
||||||
|
useCreateRecipe,
|
||||||
|
useUpdateRecipe,
|
||||||
|
useDeleteRecipe,
|
||||||
|
useDuplicateRecipe,
|
||||||
|
useActivateRecipe,
|
||||||
|
useRecipeProductionBatch,
|
||||||
|
useRecipeProductionBatches,
|
||||||
|
useRecipeProductionBatchesByRecipe,
|
||||||
|
useCreateRecipeProductionBatch,
|
||||||
|
useUpdateRecipeProductionBatch,
|
||||||
|
useDeleteRecipeProductionBatch,
|
||||||
|
useStartRecipeProductionBatch,
|
||||||
|
useCompleteRecipeProductionBatch,
|
||||||
|
useCancelRecipeProductionBatch,
|
||||||
|
recipesKeys,
|
||||||
|
} from './hooks/recipes';
|
||||||
|
|
||||||
// Query Key Factories (for advanced usage)
|
// Query Key Factories (for advanced usage)
|
||||||
export {
|
export {
|
||||||
authKeys,
|
authKeys,
|
||||||
userKeys,
|
userKeys,
|
||||||
onboardingKeys,
|
onboardingKeys,
|
||||||
tenantKeys,
|
tenantKeys,
|
||||||
salesKeys,
|
salesKeys,
|
||||||
inventoryKeys,
|
inventoryKeys,
|
||||||
classificationKeys,
|
classificationKeys,
|
||||||
|
|||||||
212
frontend/src/api/services/recipes.ts
Normal file
212
frontend/src/api/services/recipes.ts
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
/**
|
||||||
|
* Recipes service - API communication layer
|
||||||
|
* Handles all recipe-related HTTP requests using the API client
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { apiClient } from '../client/apiClient';
|
||||||
|
import type {
|
||||||
|
RecipeResponse,
|
||||||
|
RecipeCreate,
|
||||||
|
RecipeUpdate,
|
||||||
|
RecipeSearchParams,
|
||||||
|
RecipeDuplicateRequest,
|
||||||
|
RecipeFeasibilityResponse,
|
||||||
|
RecipeStatisticsResponse,
|
||||||
|
RecipeCategoriesResponse,
|
||||||
|
ProductionBatchResponse,
|
||||||
|
ProductionBatchCreate,
|
||||||
|
ProductionBatchUpdate,
|
||||||
|
} from '../types/recipes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recipes API service
|
||||||
|
* All methods return promises that resolve to the response data
|
||||||
|
*/
|
||||||
|
export class RecipesService {
|
||||||
|
private readonly baseUrl = '/recipes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new recipe
|
||||||
|
*/
|
||||||
|
async createRecipe(recipeData: RecipeCreate): Promise<RecipeResponse> {
|
||||||
|
return apiClient.post<RecipeResponse>(this.baseUrl, recipeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recipe by ID with ingredients
|
||||||
|
*/
|
||||||
|
async getRecipe(recipeId: string): Promise<RecipeResponse> {
|
||||||
|
return apiClient.get<RecipeResponse>(`${this.baseUrl}/${recipeId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing recipe
|
||||||
|
*/
|
||||||
|
async updateRecipe(recipeId: string, recipeData: RecipeUpdate): Promise<RecipeResponse> {
|
||||||
|
return apiClient.put<RecipeResponse>(`${this.baseUrl}/${recipeId}`, recipeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a recipe
|
||||||
|
*/
|
||||||
|
async deleteRecipe(recipeId: string): Promise<{ message: string }> {
|
||||||
|
return apiClient.delete<{ message: string }>(`${this.baseUrl}/${recipeId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search recipes with filters
|
||||||
|
*/
|
||||||
|
async searchRecipes(params: RecipeSearchParams = {}): Promise<RecipeResponse[]> {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
searchParams.append(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryString = searchParams.toString();
|
||||||
|
const url = queryString ? `${this.baseUrl}?${queryString}` : this.baseUrl;
|
||||||
|
|
||||||
|
return apiClient.get<RecipeResponse[]>(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all recipes (shorthand for search without filters)
|
||||||
|
*/
|
||||||
|
async getRecipes(): Promise<RecipeResponse[]> {
|
||||||
|
return this.searchRecipes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate an existing recipe
|
||||||
|
*/
|
||||||
|
async duplicateRecipe(recipeId: string, duplicateData: RecipeDuplicateRequest): Promise<RecipeResponse> {
|
||||||
|
return apiClient.post<RecipeResponse>(`${this.baseUrl}/${recipeId}/duplicate`, duplicateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate a recipe for production
|
||||||
|
*/
|
||||||
|
async activateRecipe(recipeId: string): Promise<RecipeResponse> {
|
||||||
|
return apiClient.post<RecipeResponse>(`${this.baseUrl}/${recipeId}/activate`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if recipe can be produced with current inventory
|
||||||
|
*/
|
||||||
|
async checkRecipeFeasibility(recipeId: string, batchMultiplier: number = 1.0): Promise<RecipeFeasibilityResponse> {
|
||||||
|
const params = new URLSearchParams({ batch_multiplier: String(batchMultiplier) });
|
||||||
|
return apiClient.get<RecipeFeasibilityResponse>(`${this.baseUrl}/${recipeId}/feasibility?${params}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recipe statistics for dashboard
|
||||||
|
*/
|
||||||
|
async getRecipeStatistics(): Promise<RecipeStatisticsResponse> {
|
||||||
|
return apiClient.get<RecipeStatisticsResponse>(`${this.baseUrl}/statistics/dashboard`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of recipe categories used by tenant
|
||||||
|
*/
|
||||||
|
async getRecipeCategories(): Promise<RecipeCategoriesResponse> {
|
||||||
|
return apiClient.get<RecipeCategoriesResponse>(`${this.baseUrl}/categories/list`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Production Batch Methods
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a production batch for a recipe
|
||||||
|
*/
|
||||||
|
async createProductionBatch(batchData: ProductionBatchCreate): Promise<ProductionBatchResponse> {
|
||||||
|
return apiClient.post<ProductionBatchResponse>('/production/batches', batchData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get production batch by ID
|
||||||
|
*/
|
||||||
|
async getProductionBatch(batchId: string): Promise<ProductionBatchResponse> {
|
||||||
|
return apiClient.get<ProductionBatchResponse>(`/production/batches/${batchId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update production batch
|
||||||
|
*/
|
||||||
|
async updateProductionBatch(batchId: string, batchData: ProductionBatchUpdate): Promise<ProductionBatchResponse> {
|
||||||
|
return apiClient.put<ProductionBatchResponse>(`/production/batches/${batchId}`, batchData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete production batch
|
||||||
|
*/
|
||||||
|
async deleteProductionBatch(batchId: string): Promise<{ message: string }> {
|
||||||
|
return apiClient.delete<{ message: string }>(`/production/batches/${batchId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get production batches for a recipe
|
||||||
|
*/
|
||||||
|
async getRecipeProductionBatches(recipeId: string): Promise<ProductionBatchResponse[]> {
|
||||||
|
return apiClient.get<ProductionBatchResponse[]>(`/production/batches?recipe_id=${recipeId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all production batches with optional filtering
|
||||||
|
*/
|
||||||
|
async getProductionBatches(params: {
|
||||||
|
recipe_id?: string;
|
||||||
|
status?: string;
|
||||||
|
start_date?: string;
|
||||||
|
end_date?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
} = {}): Promise<ProductionBatchResponse[]> {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
searchParams.append(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryString = searchParams.toString();
|
||||||
|
const url = queryString ? `/production/batches?${queryString}` : '/production/batches';
|
||||||
|
|
||||||
|
return apiClient.get<ProductionBatchResponse[]>(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start production batch
|
||||||
|
*/
|
||||||
|
async startProductionBatch(batchId: string): Promise<ProductionBatchResponse> {
|
||||||
|
return apiClient.post<ProductionBatchResponse>(`/production/batches/${batchId}/start`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete production batch
|
||||||
|
*/
|
||||||
|
async completeProductionBatch(
|
||||||
|
batchId: string,
|
||||||
|
completionData: {
|
||||||
|
actual_quantity?: number;
|
||||||
|
quality_score?: number;
|
||||||
|
quality_notes?: string;
|
||||||
|
waste_quantity?: number;
|
||||||
|
waste_reason?: string;
|
||||||
|
}
|
||||||
|
): Promise<ProductionBatchResponse> {
|
||||||
|
return apiClient.post<ProductionBatchResponse>(`/production/batches/${batchId}/complete`, completionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel production batch
|
||||||
|
*/
|
||||||
|
async cancelProductionBatch(batchId: string, reason?: string): Promise<ProductionBatchResponse> {
|
||||||
|
return apiClient.post<ProductionBatchResponse>(`/production/batches/${batchId}/cancel`, { reason });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and export singleton instance
|
||||||
|
export const recipesService = new RecipesService();
|
||||||
|
export default recipesService;
|
||||||
382
frontend/src/api/types/recipes.ts
Normal file
382
frontend/src/api/types/recipes.ts
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
/**
|
||||||
|
* TypeScript types for Recipes service
|
||||||
|
* Generated based on backend schemas in services/recipes/app/schemas/recipes.py
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum RecipeStatus {
|
||||||
|
DRAFT = 'draft',
|
||||||
|
ACTIVE = 'active',
|
||||||
|
TESTING = 'testing',
|
||||||
|
ARCHIVED = 'archived',
|
||||||
|
DISCONTINUED = 'discontinued'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MeasurementUnit {
|
||||||
|
GRAMS = 'g',
|
||||||
|
KILOGRAMS = 'kg',
|
||||||
|
MILLILITERS = 'ml',
|
||||||
|
LITERS = 'l',
|
||||||
|
CUPS = 'cups',
|
||||||
|
TABLESPOONS = 'tbsp',
|
||||||
|
TEASPOONS = 'tsp',
|
||||||
|
UNITS = 'units',
|
||||||
|
PIECES = 'pieces',
|
||||||
|
PERCENTAGE = '%'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ProductionStatus {
|
||||||
|
PLANNED = 'planned',
|
||||||
|
IN_PROGRESS = 'in_progress',
|
||||||
|
COMPLETED = 'completed',
|
||||||
|
FAILED = 'failed',
|
||||||
|
CANCELLED = 'cancelled'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ProductionPriority {
|
||||||
|
LOW = 'low',
|
||||||
|
NORMAL = 'normal',
|
||||||
|
HIGH = 'high',
|
||||||
|
URGENT = 'urgent'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecipeIngredientCreate {
|
||||||
|
ingredient_id: string;
|
||||||
|
quantity: number;
|
||||||
|
unit: MeasurementUnit;
|
||||||
|
alternative_quantity?: number | null;
|
||||||
|
alternative_unit?: MeasurementUnit | null;
|
||||||
|
preparation_method?: string | null;
|
||||||
|
ingredient_notes?: string | null;
|
||||||
|
is_optional: boolean;
|
||||||
|
ingredient_order: number;
|
||||||
|
ingredient_group?: string | null;
|
||||||
|
substitution_options?: Record<string, any> | null;
|
||||||
|
substitution_ratio?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecipeIngredientUpdate {
|
||||||
|
ingredient_id?: string | null;
|
||||||
|
quantity?: number | null;
|
||||||
|
unit?: MeasurementUnit | null;
|
||||||
|
alternative_quantity?: number | null;
|
||||||
|
alternative_unit?: MeasurementUnit | null;
|
||||||
|
preparation_method?: string | null;
|
||||||
|
ingredient_notes?: string | null;
|
||||||
|
is_optional?: boolean | null;
|
||||||
|
ingredient_order?: number | null;
|
||||||
|
ingredient_group?: string | null;
|
||||||
|
substitution_options?: Record<string, any> | null;
|
||||||
|
substitution_ratio?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecipeIngredientResponse {
|
||||||
|
id: string;
|
||||||
|
tenant_id: string;
|
||||||
|
recipe_id: string;
|
||||||
|
ingredient_id: string;
|
||||||
|
quantity: number;
|
||||||
|
unit: string;
|
||||||
|
quantity_in_base_unit?: number | null;
|
||||||
|
alternative_quantity?: number | null;
|
||||||
|
alternative_unit?: string | null;
|
||||||
|
preparation_method?: string | null;
|
||||||
|
ingredient_notes?: string | null;
|
||||||
|
is_optional: boolean;
|
||||||
|
ingredient_order: number;
|
||||||
|
ingredient_group?: string | null;
|
||||||
|
substitution_options?: Record<string, any> | null;
|
||||||
|
substitution_ratio?: number | null;
|
||||||
|
unit_cost?: number | null;
|
||||||
|
total_cost?: number | null;
|
||||||
|
cost_updated_at?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecipeCreate {
|
||||||
|
name: string;
|
||||||
|
recipe_code?: string | null;
|
||||||
|
version?: string;
|
||||||
|
finished_product_id: string;
|
||||||
|
description?: string | null;
|
||||||
|
category?: string | null;
|
||||||
|
cuisine_type?: string | null;
|
||||||
|
difficulty_level?: number;
|
||||||
|
yield_quantity: number;
|
||||||
|
yield_unit: MeasurementUnit;
|
||||||
|
prep_time_minutes?: number | null;
|
||||||
|
cook_time_minutes?: number | null;
|
||||||
|
total_time_minutes?: number | null;
|
||||||
|
rest_time_minutes?: number | null;
|
||||||
|
instructions?: Record<string, any> | null;
|
||||||
|
preparation_notes?: string | null;
|
||||||
|
storage_instructions?: string | null;
|
||||||
|
quality_standards?: string | null;
|
||||||
|
serves_count?: number | null;
|
||||||
|
nutritional_info?: Record<string, any> | null;
|
||||||
|
allergen_info?: Record<string, any> | null;
|
||||||
|
dietary_tags?: Record<string, any> | null;
|
||||||
|
batch_size_multiplier?: number;
|
||||||
|
minimum_batch_size?: number | null;
|
||||||
|
maximum_batch_size?: number | null;
|
||||||
|
optimal_production_temperature?: number | null;
|
||||||
|
optimal_humidity?: number | null;
|
||||||
|
quality_check_points?: Record<string, any> | null;
|
||||||
|
common_issues?: Record<string, any> | null;
|
||||||
|
is_seasonal?: boolean;
|
||||||
|
season_start_month?: number | null;
|
||||||
|
season_end_month?: number | null;
|
||||||
|
is_signature_item?: boolean;
|
||||||
|
target_margin_percentage?: number | null;
|
||||||
|
ingredients: RecipeIngredientCreate[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecipeUpdate {
|
||||||
|
name?: string | null;
|
||||||
|
recipe_code?: string | null;
|
||||||
|
version?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
category?: string | null;
|
||||||
|
cuisine_type?: string | null;
|
||||||
|
difficulty_level?: number | null;
|
||||||
|
yield_quantity?: number | null;
|
||||||
|
yield_unit?: MeasurementUnit | null;
|
||||||
|
prep_time_minutes?: number | null;
|
||||||
|
cook_time_minutes?: number | null;
|
||||||
|
total_time_minutes?: number | null;
|
||||||
|
rest_time_minutes?: number | null;
|
||||||
|
instructions?: Record<string, any> | null;
|
||||||
|
preparation_notes?: string | null;
|
||||||
|
storage_instructions?: string | null;
|
||||||
|
quality_standards?: string | null;
|
||||||
|
serves_count?: number | null;
|
||||||
|
nutritional_info?: Record<string, any> | null;
|
||||||
|
allergen_info?: Record<string, any> | null;
|
||||||
|
dietary_tags?: Record<string, any> | null;
|
||||||
|
batch_size_multiplier?: number | null;
|
||||||
|
minimum_batch_size?: number | null;
|
||||||
|
maximum_batch_size?: number | null;
|
||||||
|
optimal_production_temperature?: number | null;
|
||||||
|
optimal_humidity?: number | null;
|
||||||
|
quality_check_points?: Record<string, any> | null;
|
||||||
|
common_issues?: Record<string, any> | null;
|
||||||
|
status?: RecipeStatus | null;
|
||||||
|
is_seasonal?: boolean | null;
|
||||||
|
season_start_month?: number | null;
|
||||||
|
season_end_month?: number | null;
|
||||||
|
is_signature_item?: boolean | null;
|
||||||
|
target_margin_percentage?: number | null;
|
||||||
|
ingredients?: RecipeIngredientCreate[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecipeResponse {
|
||||||
|
id: string;
|
||||||
|
tenant_id: string;
|
||||||
|
name: string;
|
||||||
|
recipe_code?: string | null;
|
||||||
|
version: string;
|
||||||
|
finished_product_id: string;
|
||||||
|
description?: string | null;
|
||||||
|
category?: string | null;
|
||||||
|
cuisine_type?: string | null;
|
||||||
|
difficulty_level: number;
|
||||||
|
yield_quantity: number;
|
||||||
|
yield_unit: string;
|
||||||
|
prep_time_minutes?: number | null;
|
||||||
|
cook_time_minutes?: number | null;
|
||||||
|
total_time_minutes?: number | null;
|
||||||
|
rest_time_minutes?: number | null;
|
||||||
|
estimated_cost_per_unit?: number | null;
|
||||||
|
last_calculated_cost?: number | null;
|
||||||
|
cost_calculation_date?: string | null;
|
||||||
|
target_margin_percentage?: number | null;
|
||||||
|
suggested_selling_price?: number | null;
|
||||||
|
instructions?: Record<string, any> | null;
|
||||||
|
preparation_notes?: string | null;
|
||||||
|
storage_instructions?: string | null;
|
||||||
|
quality_standards?: string | null;
|
||||||
|
serves_count?: number | null;
|
||||||
|
nutritional_info?: Record<string, any> | null;
|
||||||
|
allergen_info?: Record<string, any> | null;
|
||||||
|
dietary_tags?: Record<string, any> | null;
|
||||||
|
batch_size_multiplier: number;
|
||||||
|
minimum_batch_size?: number | null;
|
||||||
|
maximum_batch_size?: number | null;
|
||||||
|
optimal_production_temperature?: number | null;
|
||||||
|
optimal_humidity?: number | null;
|
||||||
|
quality_check_points?: Record<string, any> | null;
|
||||||
|
common_issues?: Record<string, any> | null;
|
||||||
|
status: string;
|
||||||
|
is_seasonal: boolean;
|
||||||
|
season_start_month?: number | null;
|
||||||
|
season_end_month?: number | null;
|
||||||
|
is_signature_item: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
created_by?: string | null;
|
||||||
|
updated_by?: string | null;
|
||||||
|
ingredients?: RecipeIngredientResponse[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecipeSearchRequest {
|
||||||
|
search_term?: string | null;
|
||||||
|
status?: RecipeStatus | null;
|
||||||
|
category?: string | null;
|
||||||
|
is_seasonal?: boolean | null;
|
||||||
|
is_signature?: boolean | null;
|
||||||
|
difficulty_level?: number | null;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecipeSearchParams {
|
||||||
|
search_term?: string;
|
||||||
|
status?: string;
|
||||||
|
category?: string;
|
||||||
|
is_seasonal?: boolean;
|
||||||
|
is_signature?: boolean;
|
||||||
|
difficulty_level?: number;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecipeDuplicateRequest {
|
||||||
|
new_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecipeFeasibilityResponse {
|
||||||
|
recipe_id: string;
|
||||||
|
recipe_name: string;
|
||||||
|
batch_multiplier: number;
|
||||||
|
feasible: boolean;
|
||||||
|
missing_ingredients: Array<Record<string, any>>;
|
||||||
|
insufficient_ingredients: Array<Record<string, any>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecipeStatisticsResponse {
|
||||||
|
total_recipes: number;
|
||||||
|
active_recipes: number;
|
||||||
|
signature_recipes: number;
|
||||||
|
seasonal_recipes: number;
|
||||||
|
category_breakdown: Array<Record<string, any>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecipeCategoriesResponse {
|
||||||
|
categories: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Production Batch Types
|
||||||
|
export interface ProductionBatchCreate {
|
||||||
|
recipe_id: string;
|
||||||
|
batch_number: string;
|
||||||
|
production_date: string;
|
||||||
|
planned_start_time?: string | null;
|
||||||
|
planned_end_time?: string | null;
|
||||||
|
planned_quantity: number;
|
||||||
|
batch_size_multiplier?: number;
|
||||||
|
priority?: ProductionPriority;
|
||||||
|
assigned_staff?: Array<Record<string, any>> | null;
|
||||||
|
production_notes?: string | null;
|
||||||
|
customer_order_reference?: string | null;
|
||||||
|
pre_order_quantity?: number | null;
|
||||||
|
shelf_quantity?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductionBatchUpdate {
|
||||||
|
batch_number?: string | null;
|
||||||
|
production_date?: string | null;
|
||||||
|
planned_start_time?: string | null;
|
||||||
|
actual_start_time?: string | null;
|
||||||
|
planned_end_time?: string | null;
|
||||||
|
actual_end_time?: string | null;
|
||||||
|
planned_quantity?: number | null;
|
||||||
|
actual_quantity?: number | null;
|
||||||
|
batch_size_multiplier?: number | null;
|
||||||
|
status?: ProductionStatus | null;
|
||||||
|
priority?: ProductionPriority | null;
|
||||||
|
assigned_staff?: Array<Record<string, any>> | null;
|
||||||
|
production_notes?: string | null;
|
||||||
|
quality_score?: number | null;
|
||||||
|
quality_notes?: string | null;
|
||||||
|
defect_rate?: number | null;
|
||||||
|
rework_required?: boolean | null;
|
||||||
|
production_temperature?: number | null;
|
||||||
|
production_humidity?: number | null;
|
||||||
|
oven_temperature?: number | null;
|
||||||
|
baking_time_minutes?: number | null;
|
||||||
|
waste_quantity?: number | null;
|
||||||
|
waste_reason?: string | null;
|
||||||
|
customer_order_reference?: string | null;
|
||||||
|
pre_order_quantity?: number | null;
|
||||||
|
shelf_quantity?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductionBatchResponse {
|
||||||
|
id: string;
|
||||||
|
tenant_id: string;
|
||||||
|
recipe_id: string;
|
||||||
|
batch_number: string;
|
||||||
|
production_date: string;
|
||||||
|
planned_start_time?: string | null;
|
||||||
|
actual_start_time?: string | null;
|
||||||
|
planned_end_time?: string | null;
|
||||||
|
actual_end_time?: string | null;
|
||||||
|
planned_quantity: number;
|
||||||
|
actual_quantity?: number | null;
|
||||||
|
yield_percentage?: number | null;
|
||||||
|
batch_size_multiplier: number;
|
||||||
|
status: string;
|
||||||
|
priority: string;
|
||||||
|
assigned_staff?: Array<Record<string, any>> | null;
|
||||||
|
production_notes?: string | null;
|
||||||
|
quality_score?: number | null;
|
||||||
|
quality_notes?: string | null;
|
||||||
|
defect_rate?: number | null;
|
||||||
|
rework_required: boolean;
|
||||||
|
planned_material_cost?: number | null;
|
||||||
|
actual_material_cost?: number | null;
|
||||||
|
labor_cost?: number | null;
|
||||||
|
overhead_cost?: number | null;
|
||||||
|
total_production_cost?: number | null;
|
||||||
|
cost_per_unit?: number | null;
|
||||||
|
production_temperature?: number | null;
|
||||||
|
production_humidity?: number | null;
|
||||||
|
oven_temperature?: number | null;
|
||||||
|
baking_time_minutes?: number | null;
|
||||||
|
waste_quantity: number;
|
||||||
|
waste_reason?: string | null;
|
||||||
|
efficiency_percentage?: number | null;
|
||||||
|
customer_order_reference?: string | null;
|
||||||
|
pre_order_quantity?: number | null;
|
||||||
|
shelf_quantity?: number | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
created_by?: string | null;
|
||||||
|
completed_by?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error types
|
||||||
|
export interface ApiErrorDetail {
|
||||||
|
message: string;
|
||||||
|
status?: number;
|
||||||
|
code?: string;
|
||||||
|
details?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common query parameters for list endpoints
|
||||||
|
export interface PaginationParams {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DateRangeParams {
|
||||||
|
start_date?: string;
|
||||||
|
end_date?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility types for better type inference
|
||||||
|
export type RecipeFormData = Omit<RecipeCreate, 'ingredients'> & {
|
||||||
|
ingredients: Array<Omit<RecipeIngredientCreate, 'ingredient_order'> & { ingredient_order?: number }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RecipeUpdateFormData = Omit<RecipeUpdate, 'ingredients'> & {
|
||||||
|
ingredients?: Array<Omit<RecipeIngredientCreate, 'ingredient_order'> & { ingredient_order?: number }>;
|
||||||
|
};
|
||||||
267
frontend/src/locales/es/recipes.json
Normal file
267
frontend/src/locales/es/recipes.json
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
{
|
||||||
|
"title": "Gestión de Recetas",
|
||||||
|
"subtitle": "Administra las recetas de tu panadería",
|
||||||
|
"navigation": {
|
||||||
|
"all_recipes": "Todas las Recetas",
|
||||||
|
"active_recipes": "Recetas Activas",
|
||||||
|
"draft_recipes": "Borradores",
|
||||||
|
"signature_recipes": "Recetas Estrella",
|
||||||
|
"seasonal_recipes": "Recetas de Temporada",
|
||||||
|
"production_batches": "Lotes de Producción"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"create_recipe": "Crear Receta",
|
||||||
|
"edit_recipe": "Editar Receta",
|
||||||
|
"duplicate_recipe": "Duplicar Receta",
|
||||||
|
"activate_recipe": "Activar Receta",
|
||||||
|
"archive_recipe": "Archivar Receta",
|
||||||
|
"delete_recipe": "Eliminar Receta",
|
||||||
|
"view_recipe": "Ver Receta",
|
||||||
|
"check_feasibility": "Verificar Factibilidad",
|
||||||
|
"create_batch": "Crear Lote",
|
||||||
|
"start_production": "Iniciar Producción",
|
||||||
|
"complete_batch": "Completar Lote",
|
||||||
|
"cancel_batch": "Cancelar Lote",
|
||||||
|
"export_recipe": "Exportar Receta",
|
||||||
|
"print_recipe": "Imprimir Receta"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"name": "Nombre de la Receta",
|
||||||
|
"recipe_code": "Código de Receta",
|
||||||
|
"version": "Versión",
|
||||||
|
"description": "Descripción",
|
||||||
|
"category": "Categoría",
|
||||||
|
"cuisine_type": "Tipo de Cocina",
|
||||||
|
"difficulty_level": "Nivel de Dificultad",
|
||||||
|
"yield_quantity": "Cantidad de Producción",
|
||||||
|
"yield_unit": "Unidad de Producción",
|
||||||
|
"prep_time": "Tiempo de Preparación",
|
||||||
|
"cook_time": "Tiempo de Cocción",
|
||||||
|
"total_time": "Tiempo Total",
|
||||||
|
"rest_time": "Tiempo de Reposo",
|
||||||
|
"instructions": "Instrucciones",
|
||||||
|
"preparation_notes": "Notas de Preparación",
|
||||||
|
"storage_instructions": "Instrucciones de Almacenamiento",
|
||||||
|
"quality_standards": "Estándares de Calidad",
|
||||||
|
"serves_count": "Número de Porciones",
|
||||||
|
"is_seasonal": "Es de Temporada",
|
||||||
|
"season_start": "Inicio de Temporada",
|
||||||
|
"season_end": "Fin de Temporada",
|
||||||
|
"is_signature": "Es Receta Estrella",
|
||||||
|
"target_margin": "Margen Objetivo",
|
||||||
|
"batch_multiplier": "Multiplicador de Lote",
|
||||||
|
"min_batch_size": "Tamaño Mínimo de Lote",
|
||||||
|
"max_batch_size": "Tamaño Máximo de Lote",
|
||||||
|
"optimal_temperature": "Temperatura Óptima",
|
||||||
|
"optimal_humidity": "Humedad Óptima",
|
||||||
|
"allergens": "Alérgenos",
|
||||||
|
"dietary_tags": "Etiquetas Dietéticas",
|
||||||
|
"nutritional_info": "Información Nutricional"
|
||||||
|
},
|
||||||
|
"ingredients": {
|
||||||
|
"title": "Ingredientes",
|
||||||
|
"add_ingredient": "Agregar Ingrediente",
|
||||||
|
"remove_ingredient": "Eliminar Ingrediente",
|
||||||
|
"ingredient_name": "Nombre del Ingrediente",
|
||||||
|
"quantity": "Cantidad",
|
||||||
|
"unit": "Unidad",
|
||||||
|
"alternative_quantity": "Cantidad Alternativa",
|
||||||
|
"alternative_unit": "Unidad Alternativa",
|
||||||
|
"preparation_method": "Método de Preparación",
|
||||||
|
"notes": "Notas del Ingrediente",
|
||||||
|
"is_optional": "Es Opcional",
|
||||||
|
"ingredient_order": "Orden",
|
||||||
|
"ingredient_group": "Grupo",
|
||||||
|
"substitutions": "Sustituciones",
|
||||||
|
"substitution_ratio": "Proporción de Sustitución",
|
||||||
|
"cost_per_unit": "Costo por Unidad",
|
||||||
|
"total_cost": "Costo Total",
|
||||||
|
"groups": {
|
||||||
|
"wet_ingredients": "Ingredientes Húmedos",
|
||||||
|
"dry_ingredients": "Ingredientes Secos",
|
||||||
|
"spices": "Especias y Condimentos",
|
||||||
|
"toppings": "Coberturas",
|
||||||
|
"fillings": "Rellenos",
|
||||||
|
"decorations": "Decoraciones"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"draft": "Borrador",
|
||||||
|
"active": "Activa",
|
||||||
|
"testing": "En Pruebas",
|
||||||
|
"archived": "Archivada",
|
||||||
|
"discontinued": "Descontinuada"
|
||||||
|
},
|
||||||
|
"difficulty": {
|
||||||
|
"1": "Muy Fácil",
|
||||||
|
"2": "Fácil",
|
||||||
|
"3": "Intermedio",
|
||||||
|
"4": "Difícil",
|
||||||
|
"5": "Muy Difícil"
|
||||||
|
},
|
||||||
|
"units": {
|
||||||
|
"g": "gramos",
|
||||||
|
"kg": "kilogramos",
|
||||||
|
"ml": "mililitros",
|
||||||
|
"l": "litros",
|
||||||
|
"cups": "tazas",
|
||||||
|
"tbsp": "cucharadas",
|
||||||
|
"tsp": "cucharaditas",
|
||||||
|
"units": "unidades",
|
||||||
|
"pieces": "piezas",
|
||||||
|
"%": "porcentaje"
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"bread": "Panes",
|
||||||
|
"pastry": "Bollería",
|
||||||
|
"cake": "Tartas y Pasteles",
|
||||||
|
"cookies": "Galletas",
|
||||||
|
"savory": "Salados",
|
||||||
|
"desserts": "Postres",
|
||||||
|
"seasonal": "Temporada",
|
||||||
|
"specialty": "Especialidades"
|
||||||
|
},
|
||||||
|
"dietary_tags": {
|
||||||
|
"vegan": "Vegano",
|
||||||
|
"vegetarian": "Vegetariano",
|
||||||
|
"gluten_free": "Sin Gluten",
|
||||||
|
"dairy_free": "Sin Lácteos",
|
||||||
|
"nut_free": "Sin Frutos Secos",
|
||||||
|
"sugar_free": "Sin Azúcar",
|
||||||
|
"low_carb": "Bajo en Carbohidratos",
|
||||||
|
"keto": "Cetogénico",
|
||||||
|
"organic": "Orgánico"
|
||||||
|
},
|
||||||
|
"allergens": {
|
||||||
|
"gluten": "Gluten",
|
||||||
|
"dairy": "Lácteos",
|
||||||
|
"eggs": "Huevos",
|
||||||
|
"nuts": "Frutos Secos",
|
||||||
|
"soy": "Soja",
|
||||||
|
"sesame": "Sésamo",
|
||||||
|
"fish": "Pescado",
|
||||||
|
"shellfish": "Mariscos"
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"title": "Producción",
|
||||||
|
"batch_number": "Número de Lote",
|
||||||
|
"production_date": "Fecha de Producción",
|
||||||
|
"planned_quantity": "Cantidad Planificada",
|
||||||
|
"actual_quantity": "Cantidad Real",
|
||||||
|
"yield_percentage": "Porcentaje de Rendimiento",
|
||||||
|
"priority": "Prioridad",
|
||||||
|
"assigned_staff": "Personal Asignado",
|
||||||
|
"production_notes": "Notas de Producción",
|
||||||
|
"quality_score": "Puntuación de Calidad",
|
||||||
|
"quality_notes": "Notas de Calidad",
|
||||||
|
"defect_rate": "Tasa de Defectos",
|
||||||
|
"rework_required": "Requiere Reelaboración",
|
||||||
|
"waste_quantity": "Cantidad de Desperdicio",
|
||||||
|
"waste_reason": "Razón del Desperdicio",
|
||||||
|
"efficiency": "Eficiencia",
|
||||||
|
"material_cost": "Costo de Materiales",
|
||||||
|
"labor_cost": "Costo de Mano de Obra",
|
||||||
|
"overhead_cost": "Gastos Generales",
|
||||||
|
"total_cost": "Costo Total",
|
||||||
|
"cost_per_unit": "Costo por Unidad",
|
||||||
|
"status": {
|
||||||
|
"planned": "Planificado",
|
||||||
|
"in_progress": "En Proceso",
|
||||||
|
"completed": "Completado",
|
||||||
|
"failed": "Fallido",
|
||||||
|
"cancelled": "Cancelado"
|
||||||
|
},
|
||||||
|
"priority": {
|
||||||
|
"low": "Baja",
|
||||||
|
"normal": "Normal",
|
||||||
|
"high": "Alta",
|
||||||
|
"urgent": "Urgente"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"feasibility": {
|
||||||
|
"title": "Verificación de Factibilidad",
|
||||||
|
"feasible": "Factible",
|
||||||
|
"not_feasible": "No Factible",
|
||||||
|
"missing_ingredients": "Ingredientes Faltantes",
|
||||||
|
"insufficient_ingredients": "Ingredientes Insuficientes",
|
||||||
|
"batch_multiplier": "Multiplicador de Lote",
|
||||||
|
"required_quantity": "Cantidad Requerida",
|
||||||
|
"available_quantity": "Cantidad Disponible",
|
||||||
|
"shortage": "Faltante"
|
||||||
|
},
|
||||||
|
"statistics": {
|
||||||
|
"title": "Estadísticas de Recetas",
|
||||||
|
"total_recipes": "Total de Recetas",
|
||||||
|
"active_recipes": "Recetas Activas",
|
||||||
|
"signature_recipes": "Recetas Estrella",
|
||||||
|
"seasonal_recipes": "Recetas de Temporada",
|
||||||
|
"category_breakdown": "Desglose por Categoría",
|
||||||
|
"most_popular": "Más Populares",
|
||||||
|
"most_profitable": "Más Rentables",
|
||||||
|
"production_volume": "Volumen de Producción"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"all": "Todas",
|
||||||
|
"search_placeholder": "Buscar recetas...",
|
||||||
|
"status_filter": "Filtrar por Estado",
|
||||||
|
"category_filter": "Filtrar por Categoría",
|
||||||
|
"difficulty_filter": "Filtrar por Dificultad",
|
||||||
|
"seasonal_filter": "Solo Recetas de Temporada",
|
||||||
|
"signature_filter": "Solo Recetas Estrella",
|
||||||
|
"clear_filters": "Limpiar Filtros"
|
||||||
|
},
|
||||||
|
"costs": {
|
||||||
|
"estimated_cost": "Costo Estimado",
|
||||||
|
"last_calculated": "Último Cálculo",
|
||||||
|
"suggested_price": "Precio Sugerido",
|
||||||
|
"margin_percentage": "Porcentaje de Margen",
|
||||||
|
"cost_breakdown": "Desglose de Costos",
|
||||||
|
"ingredient_costs": "Costos de Ingredientes",
|
||||||
|
"labor_costs": "Costos de Mano de Obra",
|
||||||
|
"overhead_costs": "Gastos Generales"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"recipe_created": "Receta creada exitosamente",
|
||||||
|
"recipe_updated": "Receta actualizada exitosamente",
|
||||||
|
"recipe_deleted": "Receta eliminada exitosamente",
|
||||||
|
"recipe_duplicated": "Receta duplicada exitosamente",
|
||||||
|
"recipe_activated": "Receta activada exitosamente",
|
||||||
|
"batch_created": "Lote de producción creado exitosamente",
|
||||||
|
"batch_started": "Producción iniciada exitosamente",
|
||||||
|
"batch_completed": "Lote completado exitosamente",
|
||||||
|
"batch_cancelled": "Lote cancelado exitosamente",
|
||||||
|
"feasibility_checked": "Factibilidad verificada",
|
||||||
|
"loading_recipes": "Cargando recetas...",
|
||||||
|
"loading_recipe": "Cargando receta...",
|
||||||
|
"no_recipes_found": "No se encontraron recetas",
|
||||||
|
"no_ingredients": "No hay ingredientes agregados",
|
||||||
|
"confirm_delete": "¿Estás seguro de que quieres eliminar esta receta?",
|
||||||
|
"confirm_cancel_batch": "¿Estás seguro de que quieres cancelar este lote?",
|
||||||
|
"recipe_name_required": "El nombre de la receta es requerido",
|
||||||
|
"at_least_one_ingredient": "Debe agregar al menos un ingrediente",
|
||||||
|
"invalid_quantity": "La cantidad debe ser mayor a 0",
|
||||||
|
"ingredient_required": "Debe seleccionar un ingrediente"
|
||||||
|
},
|
||||||
|
"placeholders": {
|
||||||
|
"recipe_name": "Ej: Pan de Masa Madre Clásico",
|
||||||
|
"recipe_code": "Ej: PAN-001",
|
||||||
|
"description": "Describe los aspectos únicos de esta receta...",
|
||||||
|
"preparation_notes": "Notas especiales para la preparación...",
|
||||||
|
"storage_instructions": "Cómo almacenar el producto terminado...",
|
||||||
|
"quality_standards": "Criterios de calidad para el producto final...",
|
||||||
|
"batch_number": "Ej: LOTE-20231201-001",
|
||||||
|
"production_notes": "Notas específicas para este lote...",
|
||||||
|
"quality_notes": "Observaciones sobre la calidad...",
|
||||||
|
"waste_reason": "Razón del desperdicio..."
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"difficulty_level": "Nivel de 1 (muy fácil) a 5 (muy difícil)",
|
||||||
|
"yield_quantity": "Cantidad que produce esta receta",
|
||||||
|
"batch_multiplier": "Factor para escalar la receta",
|
||||||
|
"target_margin": "Margen de ganancia objetivo en porcentaje",
|
||||||
|
"optimal_temperature": "Temperatura ideal para la producción",
|
||||||
|
"optimal_humidity": "Humedad ideal para la producción",
|
||||||
|
"is_seasonal": "Marcar si es una receta de temporada específica",
|
||||||
|
"is_signature": "Marcar si es una receta característica de la panadería"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import inventoryEs from './es/inventory.json';
|
|||||||
import foodSafetyEs from './es/foodSafety.json';
|
import foodSafetyEs from './es/foodSafety.json';
|
||||||
import suppliersEs from './es/suppliers.json';
|
import suppliersEs from './es/suppliers.json';
|
||||||
import ordersEs from './es/orders.json';
|
import ordersEs from './es/orders.json';
|
||||||
|
import recipesEs from './es/recipes.json';
|
||||||
import errorsEs from './es/errors.json';
|
import errorsEs from './es/errors.json';
|
||||||
|
|
||||||
// Translation resources by language
|
// Translation resources by language
|
||||||
@@ -16,6 +17,7 @@ export const resources = {
|
|||||||
foodSafety: foodSafetyEs,
|
foodSafety: foodSafetyEs,
|
||||||
suppliers: suppliersEs,
|
suppliers: suppliersEs,
|
||||||
orders: ordersEs,
|
orders: ordersEs,
|
||||||
|
recipes: recipesEs,
|
||||||
errors: errorsEs,
|
errors: errorsEs,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -39,7 +41,7 @@ export const languageConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Namespaces available in translations
|
// Namespaces available in translations
|
||||||
export const namespaces = ['common', 'auth', 'inventory', 'foodSafety', 'suppliers', 'orders', 'errors'] as const;
|
export const namespaces = ['common', 'auth', 'inventory', 'foodSafety', 'suppliers', 'orders', 'recipes', 'errors'] as const;
|
||||||
export type Namespace = typeof namespaces[number];
|
export type Namespace = typeof namespaces[number];
|
||||||
|
|
||||||
// Helper function to get language display name
|
// Helper function to get language display name
|
||||||
@@ -53,7 +55,7 @@ export const isSupportedLanguage = (language: string): language is SupportedLang
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Export individual language modules for direct imports
|
// Export individual language modules for direct imports
|
||||||
export { commonEs, authEs, inventoryEs, foodSafetyEs, suppliersEs, ordersEs, errorsEs };
|
export { commonEs, authEs, inventoryEs, foodSafetyEs, suppliersEs, ordersEs, recipesEs, errorsEs };
|
||||||
|
|
||||||
// Default export with all translations
|
// Default export with all translations
|
||||||
export default resources;
|
export default resources;
|
||||||
Reference in New Issue
Block a user