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

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