diff --git a/frontend/src/api/hooks/recipes.ts b/frontend/src/api/hooks/recipes.ts new file mode 100644 index 00000000..bef08c06 --- /dev/null +++ b/frontend/src/api/hooks/recipes.ts @@ -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, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + 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, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + 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 = {}, + options?: Omit, 'queryKey' | 'queryFn' | 'getNextPageParam'> +) => { + return useInfiniteQuery({ + 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, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: recipesKeys.statistics(), + queryFn: () => recipesService.getRecipeStatistics(), + staleTime: 5 * 60 * 1000, // 5 minutes + ...options, + }); +}; + +/** + * Get recipe categories + */ +export const useRecipeCategories = ( + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + 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, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + 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 +) => { + const queryClient = useQueryClient(); + + return useMutation({ + 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 +) => { + const queryClient = useQueryClient(); + + return useMutation({ + 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 +) => { + const queryClient = useQueryClient(); + + return useMutation({ + 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 +) => { + const queryClient = useQueryClient(); + + return useMutation({ + 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, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + 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, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + 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, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + 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 +) => { + const queryClient = useQueryClient(); + + return useMutation({ + 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 +) => { + const queryClient = useQueryClient(); + + return useMutation({ + 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 +) => { + const queryClient = useQueryClient(); + + return useMutation({ + 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 +) => { + const queryClient = useQueryClient(); + + return useMutation({ + 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 +) => { + const queryClient = useQueryClient(); + + return useMutation({ + 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, + }); +}; \ No newline at end of file diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 5e5cb7d8..35b8819a 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -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,12 +715,37 @@ 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, - userKeys, - onboardingKeys, - tenantKeys, +export { + authKeys, + userKeys, + onboardingKeys, + tenantKeys, salesKeys, inventoryKeys, classificationKeys, diff --git a/frontend/src/api/services/recipes.ts b/frontend/src/api/services/recipes.ts new file mode 100644 index 00000000..ceaf4913 --- /dev/null +++ b/frontend/src/api/services/recipes.ts @@ -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 { + return apiClient.post(this.baseUrl, recipeData); + } + + /** + * Get recipe by ID with ingredients + */ + async getRecipe(recipeId: string): Promise { + return apiClient.get(`${this.baseUrl}/${recipeId}`); + } + + /** + * Update an existing recipe + */ + async updateRecipe(recipeId: string, recipeData: RecipeUpdate): Promise { + return apiClient.put(`${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 { + 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(url); + } + + /** + * Get all recipes (shorthand for search without filters) + */ + async getRecipes(): Promise { + return this.searchRecipes(); + } + + /** + * Duplicate an existing recipe + */ + async duplicateRecipe(recipeId: string, duplicateData: RecipeDuplicateRequest): Promise { + return apiClient.post(`${this.baseUrl}/${recipeId}/duplicate`, duplicateData); + } + + /** + * Activate a recipe for production + */ + async activateRecipe(recipeId: string): Promise { + return apiClient.post(`${this.baseUrl}/${recipeId}/activate`); + } + + /** + * Check if recipe can be produced with current inventory + */ + async checkRecipeFeasibility(recipeId: string, batchMultiplier: number = 1.0): Promise { + const params = new URLSearchParams({ batch_multiplier: String(batchMultiplier) }); + return apiClient.get(`${this.baseUrl}/${recipeId}/feasibility?${params}`); + } + + /** + * Get recipe statistics for dashboard + */ + async getRecipeStatistics(): Promise { + return apiClient.get(`${this.baseUrl}/statistics/dashboard`); + } + + /** + * Get list of recipe categories used by tenant + */ + async getRecipeCategories(): Promise { + return apiClient.get(`${this.baseUrl}/categories/list`); + } + + // Production Batch Methods + + /** + * Create a production batch for a recipe + */ + async createProductionBatch(batchData: ProductionBatchCreate): Promise { + return apiClient.post('/production/batches', batchData); + } + + /** + * Get production batch by ID + */ + async getProductionBatch(batchId: string): Promise { + return apiClient.get(`/production/batches/${batchId}`); + } + + /** + * Update production batch + */ + async updateProductionBatch(batchId: string, batchData: ProductionBatchUpdate): Promise { + return apiClient.put(`/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 { + return apiClient.get(`/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 { + 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(url); + } + + /** + * Start production batch + */ + async startProductionBatch(batchId: string): Promise { + return apiClient.post(`/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 { + return apiClient.post(`/production/batches/${batchId}/complete`, completionData); + } + + /** + * Cancel production batch + */ + async cancelProductionBatch(batchId: string, reason?: string): Promise { + return apiClient.post(`/production/batches/${batchId}/cancel`, { reason }); + } +} + +// Create and export singleton instance +export const recipesService = new RecipesService(); +export default recipesService; \ No newline at end of file diff --git a/frontend/src/api/types/recipes.ts b/frontend/src/api/types/recipes.ts new file mode 100644 index 00000000..0f07cc26 --- /dev/null +++ b/frontend/src/api/types/recipes.ts @@ -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 | 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 | 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 | 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 | null; + preparation_notes?: string | null; + storage_instructions?: string | null; + quality_standards?: string | null; + serves_count?: number | null; + nutritional_info?: Record | null; + allergen_info?: Record | null; + dietary_tags?: Record | 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 | null; + common_issues?: Record | 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 | null; + preparation_notes?: string | null; + storage_instructions?: string | null; + quality_standards?: string | null; + serves_count?: number | null; + nutritional_info?: Record | null; + allergen_info?: Record | null; + dietary_tags?: Record | 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 | null; + common_issues?: Record | 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 | null; + preparation_notes?: string | null; + storage_instructions?: string | null; + quality_standards?: string | null; + serves_count?: number | null; + nutritional_info?: Record | null; + allergen_info?: Record | null; + dietary_tags?: Record | 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 | null; + common_issues?: Record | 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>; + insufficient_ingredients: Array>; +} + +export interface RecipeStatisticsResponse { + total_recipes: number; + active_recipes: number; + signature_recipes: number; + seasonal_recipes: number; + category_breakdown: Array>; +} + +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> | 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> | 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> | 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 & { + ingredients: Array & { ingredient_order?: number }>; +}; + +export type RecipeUpdateFormData = Omit & { + ingredients?: Array & { ingredient_order?: number }>; +}; \ No newline at end of file diff --git a/frontend/src/locales/es/recipes.json b/frontend/src/locales/es/recipes.json new file mode 100644 index 00000000..32bfae4e --- /dev/null +++ b/frontend/src/locales/es/recipes.json @@ -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" + } +} \ No newline at end of file diff --git a/frontend/src/locales/index.ts b/frontend/src/locales/index.ts index 6af17179..292df896 100644 --- a/frontend/src/locales/index.ts +++ b/frontend/src/locales/index.ts @@ -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; \ No newline at end of file