2025-09-19 16:03:24 +02:00
|
|
|
/**
|
|
|
|
|
* Recipes service - API communication layer
|
|
|
|
|
* Handles all recipe-related HTTP requests using the API client
|
2025-09-19 21:39:04 +02:00
|
|
|
* Mirrors backend endpoints exactly for tenant-dependent operations
|
2025-09-19 16:03:24 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { apiClient } from '../client/apiClient';
|
|
|
|
|
import type {
|
|
|
|
|
RecipeResponse,
|
|
|
|
|
RecipeCreate,
|
|
|
|
|
RecipeUpdate,
|
|
|
|
|
RecipeSearchParams,
|
|
|
|
|
RecipeDuplicateRequest,
|
|
|
|
|
RecipeFeasibilityResponse,
|
|
|
|
|
RecipeStatisticsResponse,
|
|
|
|
|
RecipeCategoriesResponse,
|
2025-09-24 21:54:49 +02:00
|
|
|
RecipeQualityConfiguration,
|
|
|
|
|
RecipeQualityConfigurationUpdate,
|
2025-09-19 16:03:24 +02:00
|
|
|
} from '../types/recipes';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Recipes API service
|
|
|
|
|
* All methods return promises that resolve to the response data
|
2025-09-19 21:39:04 +02:00
|
|
|
* Follows tenant-dependent routing pattern: /tenants/{tenant_id}/recipes
|
2025-09-19 16:03:24 +02:00
|
|
|
*/
|
|
|
|
|
export class RecipesService {
|
2025-09-19 21:39:04 +02:00
|
|
|
/**
|
|
|
|
|
* Get tenant-scoped base URL for recipes
|
|
|
|
|
*/
|
|
|
|
|
private getBaseUrl(tenantId: string): string {
|
|
|
|
|
return `/tenants/${tenantId}/recipes`;
|
|
|
|
|
}
|
2025-09-19 16:03:24 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a new recipe
|
2025-09-19 21:39:04 +02:00
|
|
|
* POST /tenants/{tenant_id}/recipes
|
2025-09-19 16:03:24 +02:00
|
|
|
*/
|
2025-09-19 21:39:04 +02:00
|
|
|
async createRecipe(tenantId: string, recipeData: RecipeCreate): Promise<RecipeResponse> {
|
|
|
|
|
const baseUrl = this.getBaseUrl(tenantId);
|
|
|
|
|
return apiClient.post<RecipeResponse>(baseUrl, recipeData);
|
2025-09-19 16:03:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get recipe by ID with ingredients
|
2025-09-19 21:39:04 +02:00
|
|
|
* GET /tenants/{tenant_id}/recipes/{recipe_id}
|
2025-09-19 16:03:24 +02:00
|
|
|
*/
|
2025-09-19 21:39:04 +02:00
|
|
|
async getRecipe(tenantId: string, recipeId: string): Promise<RecipeResponse> {
|
|
|
|
|
const baseUrl = this.getBaseUrl(tenantId);
|
|
|
|
|
return apiClient.get<RecipeResponse>(`${baseUrl}/${recipeId}`);
|
2025-09-19 16:03:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update an existing recipe
|
2025-09-19 21:39:04 +02:00
|
|
|
* PUT /tenants/{tenant_id}/recipes/{recipe_id}
|
2025-09-19 16:03:24 +02:00
|
|
|
*/
|
2025-09-19 21:39:04 +02:00
|
|
|
async updateRecipe(tenantId: string, recipeId: string, recipeData: RecipeUpdate): Promise<RecipeResponse> {
|
|
|
|
|
const baseUrl = this.getBaseUrl(tenantId);
|
|
|
|
|
return apiClient.put<RecipeResponse>(`${baseUrl}/${recipeId}`, recipeData);
|
2025-09-19 16:03:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete a recipe
|
2025-09-19 21:39:04 +02:00
|
|
|
* DELETE /tenants/{tenant_id}/recipes/{recipe_id}
|
2025-09-19 16:03:24 +02:00
|
|
|
*/
|
2025-09-19 21:39:04 +02:00
|
|
|
async deleteRecipe(tenantId: string, recipeId: string): Promise<{ message: string }> {
|
|
|
|
|
const baseUrl = this.getBaseUrl(tenantId);
|
|
|
|
|
return apiClient.delete<{ message: string }>(`${baseUrl}/${recipeId}`);
|
2025-09-19 16:03:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Search recipes with filters
|
2025-09-19 21:39:04 +02:00
|
|
|
* GET /tenants/{tenant_id}/recipes
|
2025-09-19 16:03:24 +02:00
|
|
|
*/
|
2025-09-19 21:39:04 +02:00
|
|
|
async searchRecipes(tenantId: string, params: RecipeSearchParams = {}): Promise<RecipeResponse[]> {
|
|
|
|
|
const baseUrl = this.getBaseUrl(tenantId);
|
2025-09-19 16:03:24 +02:00
|
|
|
const searchParams = new URLSearchParams();
|
|
|
|
|
|
2025-09-19 21:39:04 +02:00
|
|
|
// Add all non-empty parameters to the query string
|
2025-09-19 16:03:24 +02:00
|
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
|
|
|
if (value !== undefined && value !== null && value !== '') {
|
|
|
|
|
searchParams.append(key, String(value));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const queryString = searchParams.toString();
|
2025-09-19 21:39:04 +02:00
|
|
|
const url = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
2025-09-19 16:03:24 +02:00
|
|
|
|
|
|
|
|
return apiClient.get<RecipeResponse[]>(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all recipes (shorthand for search without filters)
|
2025-09-19 21:39:04 +02:00
|
|
|
* GET /tenants/{tenant_id}/recipes
|
2025-09-19 16:03:24 +02:00
|
|
|
*/
|
2025-09-19 21:39:04 +02:00
|
|
|
async getRecipes(tenantId: string): Promise<RecipeResponse[]> {
|
|
|
|
|
return this.searchRecipes(tenantId);
|
2025-09-19 16:03:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Duplicate an existing recipe
|
2025-09-19 21:39:04 +02:00
|
|
|
* POST /tenants/{tenant_id}/recipes/{recipe_id}/duplicate
|
2025-09-19 16:03:24 +02:00
|
|
|
*/
|
2025-09-19 21:39:04 +02:00
|
|
|
async duplicateRecipe(tenantId: string, recipeId: string, duplicateData: RecipeDuplicateRequest): Promise<RecipeResponse> {
|
|
|
|
|
const baseUrl = this.getBaseUrl(tenantId);
|
|
|
|
|
return apiClient.post<RecipeResponse>(`${baseUrl}/${recipeId}/duplicate`, duplicateData);
|
2025-09-19 16:03:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Activate a recipe for production
|
2025-09-19 21:39:04 +02:00
|
|
|
* POST /tenants/{tenant_id}/recipes/{recipe_id}/activate
|
2025-09-19 16:03:24 +02:00
|
|
|
*/
|
2025-09-19 21:39:04 +02:00
|
|
|
async activateRecipe(tenantId: string, recipeId: string): Promise<RecipeResponse> {
|
|
|
|
|
const baseUrl = this.getBaseUrl(tenantId);
|
|
|
|
|
return apiClient.post<RecipeResponse>(`${baseUrl}/${recipeId}/activate`);
|
2025-09-19 16:03:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if recipe can be produced with current inventory
|
2025-09-19 21:39:04 +02:00
|
|
|
* GET /tenants/{tenant_id}/recipes/{recipe_id}/feasibility
|
2025-09-19 16:03:24 +02:00
|
|
|
*/
|
2025-09-19 21:39:04 +02:00
|
|
|
async checkRecipeFeasibility(tenantId: string, recipeId: string, batchMultiplier: number = 1.0): Promise<RecipeFeasibilityResponse> {
|
|
|
|
|
const baseUrl = this.getBaseUrl(tenantId);
|
2025-09-19 16:03:24 +02:00
|
|
|
const params = new URLSearchParams({ batch_multiplier: String(batchMultiplier) });
|
2025-09-19 21:39:04 +02:00
|
|
|
return apiClient.get<RecipeFeasibilityResponse>(`${baseUrl}/${recipeId}/feasibility?${params}`);
|
2025-09-19 16:03:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get recipe statistics for dashboard
|
2025-09-19 21:39:04 +02:00
|
|
|
* GET /tenants/{tenant_id}/recipes/statistics/dashboard
|
2025-09-19 16:03:24 +02:00
|
|
|
*/
|
2025-09-19 21:39:04 +02:00
|
|
|
async getRecipeStatistics(tenantId: string): Promise<RecipeStatisticsResponse> {
|
|
|
|
|
const baseUrl = this.getBaseUrl(tenantId);
|
|
|
|
|
return apiClient.get<RecipeStatisticsResponse>(`${baseUrl}/statistics/dashboard`);
|
2025-09-19 16:03:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get list of recipe categories used by tenant
|
2025-09-19 21:39:04 +02:00
|
|
|
* GET /tenants/{tenant_id}/recipes/categories/list
|
2025-09-19 16:03:24 +02:00
|
|
|
*/
|
2025-09-19 21:39:04 +02:00
|
|
|
async getRecipeCategories(tenantId: string): Promise<RecipeCategoriesResponse> {
|
|
|
|
|
const baseUrl = this.getBaseUrl(tenantId);
|
|
|
|
|
return apiClient.get<RecipeCategoriesResponse>(`${baseUrl}/categories/list`);
|
2025-09-19 16:03:24 +02:00
|
|
|
}
|
2025-09-24 21:54:49 +02:00
|
|
|
|
|
|
|
|
// Quality Configuration Methods
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get quality configuration for a recipe
|
|
|
|
|
*/
|
|
|
|
|
async getRecipeQualityConfiguration(
|
|
|
|
|
tenantId: string,
|
|
|
|
|
recipeId: string
|
|
|
|
|
): Promise<RecipeQualityConfiguration> {
|
|
|
|
|
const baseUrl = this.getBaseUrl(tenantId);
|
|
|
|
|
return apiClient.get<RecipeQualityConfiguration>(`${baseUrl}/${recipeId}/quality-configuration`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update quality configuration for a recipe
|
|
|
|
|
*/
|
|
|
|
|
async updateRecipeQualityConfiguration(
|
|
|
|
|
tenantId: string,
|
|
|
|
|
recipeId: string,
|
|
|
|
|
qualityConfig: RecipeQualityConfigurationUpdate
|
|
|
|
|
): Promise<RecipeQualityConfiguration> {
|
|
|
|
|
const baseUrl = this.getBaseUrl(tenantId);
|
|
|
|
|
return apiClient.put<RecipeQualityConfiguration>(
|
|
|
|
|
`${baseUrl}/${recipeId}/quality-configuration`,
|
|
|
|
|
qualityConfig
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add quality templates to a recipe stage
|
|
|
|
|
*/
|
|
|
|
|
async addQualityTemplatesToStage(
|
|
|
|
|
tenantId: string,
|
|
|
|
|
recipeId: string,
|
|
|
|
|
stage: string,
|
|
|
|
|
templateIds: string[]
|
|
|
|
|
): Promise<{ message: string }> {
|
|
|
|
|
const baseUrl = this.getBaseUrl(tenantId);
|
|
|
|
|
return apiClient.post<{ message: string }>(
|
|
|
|
|
`${baseUrl}/${recipeId}/quality-configuration/stages/${stage}/templates`,
|
|
|
|
|
templateIds
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove a quality template from a recipe stage
|
|
|
|
|
*/
|
|
|
|
|
async removeQualityTemplateFromStage(
|
|
|
|
|
tenantId: string,
|
|
|
|
|
recipeId: string,
|
|
|
|
|
stage: string,
|
|
|
|
|
templateId: string
|
|
|
|
|
): Promise<{ message: string }> {
|
|
|
|
|
const baseUrl = this.getBaseUrl(tenantId);
|
|
|
|
|
return apiClient.delete<{ message: string }>(
|
|
|
|
|
`${baseUrl}/${recipeId}/quality-configuration/stages/${stage}/templates/${templateId}`
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-09-19 16:03:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create and export singleton instance
|
|
|
|
|
export const recipesService = new RecipesService();
|
|
|
|
|
export default recipesService;
|