Create the forntend API definitions for recipe service

This commit is contained in:
Urtzi Alfaro
2025-09-19 16:03:24 +02:00
parent caf6d92850
commit 2e733abed3
6 changed files with 1399 additions and 7 deletions

View 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,
});
};

View File

@@ -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,

View 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;

View 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 }>;
};

View 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"
}
}

View File

@@ -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;