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 { productionService } from './services/production';
|
||||
export { posService } from './services/pos';
|
||||
export { recipesService } from './services/recipes';
|
||||
|
||||
// Types - Auth
|
||||
export type {
|
||||
@@ -374,6 +375,31 @@ export type {
|
||||
POSEnvironment,
|
||||
} 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
|
||||
export {
|
||||
useAuthProfile,
|
||||
@@ -689,6 +715,31 @@ export {
|
||||
posKeys,
|
||||
} 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)
|
||||
export {
|
||||
authKeys,
|
||||
|
||||
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 suppliersEs from './es/suppliers.json';
|
||||
import ordersEs from './es/orders.json';
|
||||
import recipesEs from './es/recipes.json';
|
||||
import errorsEs from './es/errors.json';
|
||||
|
||||
// Translation resources by language
|
||||
@@ -16,6 +17,7 @@ export const resources = {
|
||||
foodSafety: foodSafetyEs,
|
||||
suppliers: suppliersEs,
|
||||
orders: ordersEs,
|
||||
recipes: recipesEs,
|
||||
errors: errorsEs,
|
||||
},
|
||||
};
|
||||
@@ -39,7 +41,7 @@ export const languageConfig = {
|
||||
};
|
||||
|
||||
// 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];
|
||||
|
||||
// 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 { commonEs, authEs, inventoryEs, foodSafetyEs, suppliersEs, ordersEs, errorsEs };
|
||||
export { commonEs, authEs, inventoryEs, foodSafetyEs, suppliersEs, ordersEs, recipesEs, errorsEs };
|
||||
|
||||
// Default export with all translations
|
||||
export default resources;
|
||||
Reference in New Issue
Block a user