Create new services: inventory, recipes, suppliers

This commit is contained in:
Urtzi Alfaro
2025-08-13 17:39:35 +02:00
parent fbe7470ad9
commit 16b8a9d50c
151 changed files with 35799 additions and 857 deletions

View File

@@ -13,6 +13,8 @@ import { TrainingService } from './training.service';
import { ForecastingService } from './forecasting.service';
import { NotificationService } from './notification.service';
import { OnboardingService } from './onboarding.service';
import { InventoryService } from './inventory.service';
import { RecipesService } from './recipes.service';
// Create service instances
export const authService = new AuthService();
@@ -23,6 +25,8 @@ export const trainingService = new TrainingService();
export const forecastingService = new ForecastingService();
export const notificationService = new NotificationService();
export const onboardingService = new OnboardingService();
export const inventoryService = new InventoryService();
export const recipesService = new RecipesService();
// Export the classes as well
export {
@@ -33,7 +37,9 @@ export {
TrainingService,
ForecastingService,
NotificationService,
OnboardingService
OnboardingService,
InventoryService,
RecipesService
};
// Import base client
@@ -53,6 +59,8 @@ export const api = {
forecasting: forecastingService,
notification: notificationService,
onboarding: onboardingService,
inventory: inventoryService,
recipes: recipesService,
} as const;
// Service status checking

View File

@@ -0,0 +1,474 @@
// frontend/src/api/services/inventory.service.ts
/**
* Inventory Service
* Handles inventory management, stock tracking, and product operations
*/
import { apiClient } from '../client';
// ========== TYPES AND INTERFACES ==========
export type ProductType = 'ingredient' | 'finished_product';
export type UnitOfMeasure =
| 'kilograms' | 'grams' | 'liters' | 'milliliters'
| 'units' | 'pieces' | 'dozens' | 'boxes';
export type IngredientCategory =
| 'flour' | 'yeast' | 'dairy' | 'eggs' | 'sugar'
| 'fats' | 'salt' | 'spices' | 'additives' | 'packaging';
export type ProductCategory =
| 'bread' | 'croissants' | 'pastries' | 'cakes'
| 'cookies' | 'muffins' | 'sandwiches' | 'beverages' | 'other_products';
export type StockMovementType =
| 'purchase' | 'consumption' | 'adjustment'
| 'waste' | 'transfer' | 'return';
export interface InventoryItem {
id: string;
tenant_id: string;
name: string;
product_type: ProductType;
category: IngredientCategory | ProductCategory;
unit_of_measure: UnitOfMeasure;
estimated_shelf_life_days?: number;
requires_refrigeration: boolean;
requires_freezing: boolean;
is_seasonal: boolean;
minimum_stock_level?: number;
maximum_stock_level?: number;
reorder_point?: number;
supplier?: string;
notes?: string;
barcode?: string;
cost_per_unit?: number;
is_active: boolean;
created_at: string;
updated_at: string;
// Computed fields
current_stock?: StockLevel;
low_stock_alert?: boolean;
expiring_soon_alert?: boolean;
recent_movements?: StockMovement[];
}
export interface StockLevel {
item_id: string;
current_quantity: number;
available_quantity: number;
reserved_quantity: number;
unit_of_measure: UnitOfMeasure;
value_estimate?: number;
last_updated: string;
// Batch information
batches?: StockBatch[];
oldest_batch_date?: string;
newest_batch_date?: string;
}
export interface StockBatch {
id: string;
item_id: string;
batch_number?: string;
quantity: number;
unit_cost?: number;
purchase_date?: string;
expiration_date?: string;
supplier?: string;
notes?: string;
is_expired: boolean;
days_until_expiration?: number;
}
export interface StockMovement {
id: string;
item_id: string;
movement_type: StockMovementType;
quantity: number;
unit_cost?: number;
total_cost?: number;
batch_id?: string;
reference_id?: string;
notes?: string;
movement_date: string;
created_by: string;
created_at: string;
// Related data
item_name?: string;
batch_info?: StockBatch;
}
export interface StockAlert {
id: string;
item_id: string;
alert_type: 'low_stock' | 'expired' | 'expiring_soon' | 'overstock';
severity: 'low' | 'medium' | 'high' | 'critical';
message: string;
threshold_value?: number;
current_value?: number;
is_acknowledged: boolean;
created_at: string;
acknowledged_at?: string;
acknowledged_by?: string;
// Related data
item?: InventoryItem;
}
// ========== REQUEST/RESPONSE TYPES ==========
export interface CreateInventoryItemRequest {
name: string;
product_type: ProductType;
category: IngredientCategory | ProductCategory;
unit_of_measure: UnitOfMeasure;
estimated_shelf_life_days?: number;
requires_refrigeration?: boolean;
requires_freezing?: boolean;
is_seasonal?: boolean;
minimum_stock_level?: number;
maximum_stock_level?: number;
reorder_point?: number;
supplier?: string;
notes?: string;
barcode?: string;
cost_per_unit?: number;
}
export interface UpdateInventoryItemRequest extends Partial<CreateInventoryItemRequest> {
is_active?: boolean;
}
export interface StockAdjustmentRequest {
movement_type: StockMovementType;
quantity: number;
unit_cost?: number;
batch_number?: string;
expiration_date?: string;
supplier?: string;
notes?: string;
}
export interface InventorySearchParams {
search?: string;
product_type?: ProductType;
category?: string;
is_active?: boolean;
low_stock_only?: boolean;
expiring_soon_only?: boolean;
page?: number;
limit?: number;
sort_by?: 'name' | 'category' | 'stock_level' | 'last_movement' | 'created_at';
sort_order?: 'asc' | 'desc';
}
export interface StockMovementSearchParams {
item_id?: string;
movement_type?: StockMovementType;
date_from?: string;
date_to?: string;
page?: number;
limit?: number;
}
export interface InventoryDashboardData {
total_items: number;
total_value: number;
low_stock_count: number;
expiring_soon_count: number;
recent_movements: StockMovement[];
top_items_by_value: InventoryItem[];
category_breakdown: {
category: string;
count: number;
value: number;
}[];
movement_trends: {
date: string;
purchases: number;
consumption: number;
waste: number;
}[];
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
limit: number;
total_pages: number;
}
// ========== INVENTORY SERVICE CLASS ==========
export class InventoryService {
private baseEndpoint = '/api/v1';
// ========== INVENTORY ITEMS ==========
/**
* Get inventory items with filtering and pagination
*/
async getInventoryItems(
tenantId: string,
params?: InventorySearchParams
): Promise<PaginatedResponse<InventoryItem>> {
const searchParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
searchParams.append(key, value.toString());
}
});
}
const query = searchParams.toString();
const url = `${this.baseEndpoint}/tenants/${tenantId}/inventory/items${query ? `?${query}` : ''}`;
return apiClient.get(url);
}
/**
* Get single inventory item by ID
*/
async getInventoryItem(tenantId: string, itemId: string): Promise<InventoryItem> {
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/items/${itemId}`);
}
/**
* Create new inventory item
*/
async createInventoryItem(
tenantId: string,
data: CreateInventoryItemRequest
): Promise<InventoryItem> {
return apiClient.post(`${this.baseEndpoint}/tenants/${tenantId}/inventory/items`, data);
}
/**
* Update existing inventory item
*/
async updateInventoryItem(
tenantId: string,
itemId: string,
data: UpdateInventoryItemRequest
): Promise<InventoryItem> {
return apiClient.put(`${this.baseEndpoint}/tenants/${tenantId}/inventory/items/${itemId}`, data);
}
/**
* Delete inventory item (soft delete)
*/
async deleteInventoryItem(tenantId: string, itemId: string): Promise<void> {
return apiClient.delete(`${this.baseEndpoint}/tenants/${tenantId}/inventory/items/${itemId}`);
}
/**
* Bulk update inventory items
*/
async bulkUpdateInventoryItems(
tenantId: string,
updates: { id: string; data: UpdateInventoryItemRequest }[]
): Promise<{ success: number; failed: number; errors: string[] }> {
return apiClient.post(`${this.baseEndpoint}/tenants/${tenantId}/inventory/items/bulk-update`, {
updates
});
}
// ========== STOCK MANAGEMENT ==========
/**
* Get current stock level for an item
*/
async getStockLevel(tenantId: string, itemId: string): Promise<StockLevel> {
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/stock/${itemId}`);
}
/**
* Get stock levels for all items
*/
async getAllStockLevels(tenantId: string): Promise<StockLevel[]> {
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/stock`);
}
/**
* Adjust stock level (purchase, consumption, waste, etc.)
*/
async adjustStock(
tenantId: string,
itemId: string,
adjustment: StockAdjustmentRequest
): Promise<StockMovement> {
return apiClient.post(
`${this.baseEndpoint}/tenants/${tenantId}/inventory/stock/${itemId}/adjust`,
adjustment
);
}
/**
* Bulk stock adjustments
*/
async bulkAdjustStock(
tenantId: string,
adjustments: { item_id: string; adjustment: StockAdjustmentRequest }[]
): Promise<{ success: number; failed: number; movements: StockMovement[]; errors: string[] }> {
return apiClient.post(`${this.baseEndpoint}/tenants/${tenantId}/inventory/stock/bulk-adjust`, {
adjustments
});
}
/**
* Get stock movements with filtering
*/
async getStockMovements(
tenantId: string,
params?: StockMovementSearchParams
): Promise<PaginatedResponse<StockMovement>> {
const searchParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
searchParams.append(key, value.toString());
}
});
}
const query = searchParams.toString();
const url = `${this.baseEndpoint}/tenants/${tenantId}/inventory/movements${query ? `?${query}` : ''}`;
return apiClient.get(url);
}
// ========== ALERTS ==========
/**
* Get current stock alerts
*/
async getStockAlerts(tenantId: string): Promise<StockAlert[]> {
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/alerts`);
}
/**
* Acknowledge alert
*/
async acknowledgeAlert(tenantId: string, alertId: string): Promise<void> {
return apiClient.post(`${this.baseEndpoint}/tenants/${tenantId}/inventory/alerts/${alertId}/acknowledge`);
}
/**
* Bulk acknowledge alerts
*/
async bulkAcknowledgeAlerts(tenantId: string, alertIds: string[]): Promise<void> {
return apiClient.post(`${this.baseEndpoint}/tenants/${tenantId}/inventory/alerts/bulk-acknowledge`, {
alert_ids: alertIds
});
}
// ========== DASHBOARD & ANALYTICS ==========
/**
* Get inventory dashboard data
*/
async getDashboardData(tenantId: string): Promise<InventoryDashboardData> {
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/dashboard`);
}
/**
* Get inventory value report
*/
async getInventoryValue(tenantId: string): Promise<{
total_value: number;
by_category: { category: string; value: number; percentage: number }[];
by_product_type: { type: ProductType; value: number; percentage: number }[];
}> {
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/value`);
}
/**
* Get low stock report
*/
async getLowStockReport(tenantId: string): Promise<{
items: InventoryItem[];
total_affected: number;
estimated_loss: number;
}> {
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/reports/low-stock`);
}
/**
* Get expiring items report
*/
async getExpiringItemsReport(tenantId: string, days?: number): Promise<{
items: (InventoryItem & { batches: StockBatch[] })[];
total_affected: number;
estimated_loss: number;
}> {
const params = days ? `?days=${days}` : '';
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/reports/expiring${params}`);
}
// ========== IMPORT/EXPORT ==========
/**
* Export inventory data to CSV
*/
async exportInventory(tenantId: string, format: 'csv' | 'excel' = 'csv'): Promise<Blob> {
const response = await apiClient.getRaw(
`${this.baseEndpoint}/tenants/${tenantId}/inventory/export?format=${format}`
);
return response.blob();
}
/**
* Import inventory from file
*/
async importInventory(tenantId: string, file: File): Promise<{
success: number;
failed: number;
errors: string[];
created_items: InventoryItem[];
}> {
const formData = new FormData();
formData.append('file', file);
return apiClient.post(`${this.baseEndpoint}/tenants/${tenantId}/inventory/import`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
// ========== SEARCH & SUGGESTIONS ==========
/**
* Search inventory items with autocomplete
*/
async searchItems(tenantId: string, query: string, limit = 10): Promise<InventoryItem[]> {
return apiClient.get(
`${this.baseEndpoint}/tenants/${tenantId}/inventory/search?q=${encodeURIComponent(query)}&limit=${limit}`
);
}
/**
* Get category suggestions based on product type
*/
async getCategorySuggestions(productType: ProductType): Promise<string[]> {
return apiClient.get(`${this.baseEndpoint}/inventory/categories?type=${productType}`);
}
/**
* Get supplier suggestions
*/
async getSupplierSuggestions(tenantId: string): Promise<string[]> {
return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/suppliers`);
}
}
export const inventoryService = new InventoryService();

View File

@@ -29,6 +29,61 @@ export interface UpdateStepRequest {
data?: Record<string, any>;
}
export interface InventorySuggestion {
suggestion_id: string;
original_name: string;
suggested_name: string;
product_type: 'ingredient' | 'finished_product';
category: string;
unit_of_measure: string;
confidence_score: number;
estimated_shelf_life_days?: number;
requires_refrigeration: boolean;
requires_freezing: boolean;
is_seasonal: boolean;
suggested_supplier?: string;
notes?: string;
user_approved?: boolean;
user_modifications?: Record<string, any>;
}
export interface BusinessModelAnalysis {
model: 'production' | 'retail' | 'hybrid';
confidence: number;
ingredient_count: number;
finished_product_count: number;
ingredient_ratio: number;
recommendations: string[];
}
export interface OnboardingAnalysisResult {
total_products_found: number;
inventory_suggestions: InventorySuggestion[];
business_model_analysis: BusinessModelAnalysis;
import_job_id: string;
status: string;
processed_rows: number;
errors: string[];
warnings: string[];
}
export interface InventoryCreationResult {
created_items: any[];
failed_items: any[];
total_approved: number;
success_rate: number;
}
export interface SalesImportResult {
import_job_id: string;
status: string;
processed_rows: number;
successful_imports: number;
failed_imports: number;
errors: string[];
warnings: string[];
}
export class OnboardingService {
private baseEndpoint = '/users/me/onboarding';
@@ -87,6 +142,64 @@ export class OnboardingService {
async canAccessStep(stepName: string): Promise<{ can_access: boolean; reason?: string }> {
return apiClient.get(`${this.baseEndpoint}/can-access/${stepName}`);
}
// ========== AUTOMATED INVENTORY CREATION METHODS ==========
/**
* Phase 1: Analyze sales data and get AI suggestions
*/
async analyzeSalesDataForOnboarding(tenantId: string, file: File): Promise<OnboardingAnalysisResult> {
const formData = new FormData();
formData.append('file', file);
return apiClient.post(`/tenants/${tenantId}/onboarding/analyze`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
/**
* Phase 2: Create inventory from approved suggestions
*/
async createInventoryFromSuggestions(
tenantId: string,
suggestions: InventorySuggestion[]
): Promise<InventoryCreationResult> {
return apiClient.post(`/tenants/${tenantId}/onboarding/create-inventory`, {
suggestions: suggestions.map(s => ({
suggestion_id: s.suggestion_id,
approved: s.user_approved ?? true,
modifications: s.user_modifications || {}
}))
});
}
/**
* Phase 3: Import sales data with inventory mapping
*/
async importSalesWithInventory(
tenantId: string,
file: File,
inventoryMapping: Record<string, string>
): Promise<SalesImportResult> {
const formData = new FormData();
formData.append('file', file);
formData.append('inventory_mapping', JSON.stringify(inventoryMapping));
return apiClient.post(`/tenants/${tenantId}/onboarding/import-sales`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
/**
* Get business model guidance based on analysis
*/
async getBusinessModelGuide(tenantId: string, model: string): Promise<any> {
return apiClient.get(`/tenants/${tenantId}/onboarding/business-model-guide?model=${model}`);
}
}
export const onboardingService = new OnboardingService();

View File

@@ -0,0 +1,551 @@
// frontend/src/api/services/recipes.service.ts
/**
* Recipe Service API Client
* Handles all recipe and production management API calls
*/
import { apiClient } from '../client';
import type {
PaginatedResponse,
ApiResponse,
CreateResponse,
UpdateResponse
} from '../types';
// Recipe Types
export interface Recipe {
id: string;
tenant_id: string;
name: string;
recipe_code?: string;
version: string;
finished_product_id: string;
description?: string;
category?: string;
cuisine_type?: string;
difficulty_level: number;
yield_quantity: number;
yield_unit: string;
prep_time_minutes?: number;
cook_time_minutes?: number;
total_time_minutes?: number;
rest_time_minutes?: number;
estimated_cost_per_unit?: number;
last_calculated_cost?: number;
cost_calculation_date?: string;
target_margin_percentage?: number;
suggested_selling_price?: number;
instructions?: Record<string, any>;
preparation_notes?: string;
storage_instructions?: string;
quality_standards?: string;
serves_count?: number;
nutritional_info?: Record<string, any>;
allergen_info?: Record<string, any>;
dietary_tags?: Record<string, any>;
batch_size_multiplier: number;
minimum_batch_size?: number;
maximum_batch_size?: number;
optimal_production_temperature?: number;
optimal_humidity?: number;
quality_check_points?: Record<string, any>;
common_issues?: Record<string, any>;
status: 'draft' | 'active' | 'testing' | 'archived' | 'discontinued';
is_seasonal: boolean;
season_start_month?: number;
season_end_month?: number;
is_signature_item: boolean;
created_at: string;
updated_at: string;
created_by?: string;
updated_by?: string;
ingredients?: RecipeIngredient[];
}
export interface RecipeIngredient {
id: string;
tenant_id: string;
recipe_id: string;
ingredient_id: string;
quantity: number;
unit: string;
quantity_in_base_unit?: number;
alternative_quantity?: number;
alternative_unit?: string;
preparation_method?: string;
ingredient_notes?: string;
is_optional: boolean;
ingredient_order: number;
ingredient_group?: string;
substitution_options?: Record<string, any>;
substitution_ratio?: number;
unit_cost?: number;
total_cost?: number;
cost_updated_at?: string;
}
export interface CreateRecipeRequest {
name: string;
recipe_code?: string;
version?: string;
finished_product_id: string;
description?: string;
category?: string;
cuisine_type?: string;
difficulty_level?: number;
yield_quantity: number;
yield_unit: string;
prep_time_minutes?: number;
cook_time_minutes?: number;
total_time_minutes?: number;
rest_time_minutes?: number;
instructions?: Record<string, any>;
preparation_notes?: string;
storage_instructions?: string;
quality_standards?: string;
serves_count?: number;
nutritional_info?: Record<string, any>;
allergen_info?: Record<string, any>;
dietary_tags?: Record<string, any>;
batch_size_multiplier?: number;
minimum_batch_size?: number;
maximum_batch_size?: number;
optimal_production_temperature?: number;
optimal_humidity?: number;
quality_check_points?: Record<string, any>;
common_issues?: Record<string, any>;
is_seasonal?: boolean;
season_start_month?: number;
season_end_month?: number;
is_signature_item?: boolean;
target_margin_percentage?: number;
ingredients: CreateRecipeIngredientRequest[];
}
export interface CreateRecipeIngredientRequest {
ingredient_id: string;
quantity: number;
unit: string;
alternative_quantity?: number;
alternative_unit?: string;
preparation_method?: string;
ingredient_notes?: string;
is_optional?: boolean;
ingredient_order: number;
ingredient_group?: string;
substitution_options?: Record<string, any>;
substitution_ratio?: number;
}
export interface UpdateRecipeRequest {
name?: string;
recipe_code?: string;
version?: string;
description?: string;
category?: string;
cuisine_type?: string;
difficulty_level?: number;
yield_quantity?: number;
yield_unit?: string;
prep_time_minutes?: number;
cook_time_minutes?: number;
total_time_minutes?: number;
rest_time_minutes?: number;
instructions?: Record<string, any>;
preparation_notes?: string;
storage_instructions?: string;
quality_standards?: string;
serves_count?: number;
nutritional_info?: Record<string, any>;
allergen_info?: Record<string, any>;
dietary_tags?: Record<string, any>;
batch_size_multiplier?: number;
minimum_batch_size?: number;
maximum_batch_size?: number;
optimal_production_temperature?: number;
optimal_humidity?: number;
quality_check_points?: Record<string, any>;
common_issues?: Record<string, any>;
status?: 'draft' | 'active' | 'testing' | 'archived' | 'discontinued';
is_seasonal?: boolean;
season_start_month?: number;
season_end_month?: number;
is_signature_item?: boolean;
target_margin_percentage?: number;
ingredients?: CreateRecipeIngredientRequest[];
}
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 RecipeFeasibility {
recipe_id: string;
recipe_name: string;
batch_multiplier: number;
feasible: boolean;
missing_ingredients: Array<{
ingredient_id: string;
ingredient_name: string;
required_quantity: number;
unit: string;
}>;
insufficient_ingredients: Array<{
ingredient_id: string;
ingredient_name: string;
required_quantity: number;
available_quantity: number;
unit: string;
}>;
}
export interface RecipeStatistics {
total_recipes: number;
active_recipes: number;
signature_recipes: number;
seasonal_recipes: number;
category_breakdown: Array<{
category: string;
count: number;
}>;
}
// Production Types
export interface ProductionBatch {
id: string;
tenant_id: string;
recipe_id: string;
batch_number: string;
production_date: string;
planned_start_time?: string;
actual_start_time?: string;
planned_end_time?: string;
actual_end_time?: string;
planned_quantity: number;
actual_quantity?: number;
yield_percentage?: number;
batch_size_multiplier: number;
status: 'planned' | 'in_progress' | 'completed' | 'failed' | 'cancelled';
priority: 'low' | 'normal' | 'high' | 'urgent';
assigned_staff?: string[];
production_notes?: string;
quality_score?: number;
quality_notes?: string;
defect_rate?: number;
rework_required: boolean;
planned_material_cost?: number;
actual_material_cost?: number;
labor_cost?: number;
overhead_cost?: number;
total_production_cost?: number;
cost_per_unit?: number;
production_temperature?: number;
production_humidity?: number;
oven_temperature?: number;
baking_time_minutes?: number;
waste_quantity: number;
waste_reason?: string;
efficiency_percentage?: number;
customer_order_reference?: string;
pre_order_quantity?: number;
shelf_quantity?: number;
created_at: string;
updated_at: string;
created_by?: string;
completed_by?: string;
ingredient_consumptions?: ProductionIngredientConsumption[];
}
export interface ProductionIngredientConsumption {
id: string;
tenant_id: string;
production_batch_id: string;
recipe_ingredient_id: string;
ingredient_id: string;
stock_id?: string;
planned_quantity: number;
actual_quantity: number;
unit: string;
variance_quantity?: number;
variance_percentage?: number;
unit_cost?: number;
total_cost?: number;
consumption_time: string;
consumption_notes?: string;
staff_member?: string;
ingredient_condition?: string;
quality_impact?: string;
substitution_used: boolean;
substitution_details?: string;
}
export interface CreateProductionBatchRequest {
recipe_id: string;
batch_number?: string;
production_date: string;
planned_start_time?: string;
planned_end_time?: string;
planned_quantity: number;
batch_size_multiplier?: number;
priority?: 'low' | 'normal' | 'high' | 'urgent';
assigned_staff?: string[];
production_notes?: string;
customer_order_reference?: string;
pre_order_quantity?: number;
shelf_quantity?: number;
}
export interface UpdateProductionBatchRequest {
batch_number?: string;
production_date?: string;
planned_start_time?: string;
actual_start_time?: string;
planned_end_time?: string;
actual_end_time?: string;
planned_quantity?: number;
actual_quantity?: number;
batch_size_multiplier?: number;
status?: 'planned' | 'in_progress' | 'completed' | 'failed' | 'cancelled';
priority?: 'low' | 'normal' | 'high' | 'urgent';
assigned_staff?: string[];
production_notes?: string;
quality_score?: number;
quality_notes?: string;
defect_rate?: number;
rework_required?: boolean;
labor_cost?: number;
overhead_cost?: number;
production_temperature?: number;
production_humidity?: number;
oven_temperature?: number;
baking_time_minutes?: number;
waste_quantity?: number;
waste_reason?: string;
customer_order_reference?: string;
pre_order_quantity?: number;
shelf_quantity?: number;
}
export interface ProductionBatchSearchParams {
search_term?: string;
status?: string;
priority?: string;
start_date?: string;
end_date?: string;
recipe_id?: string;
limit?: number;
offset?: number;
}
export interface ProductionStatistics {
total_batches: number;
completed_batches: number;
failed_batches: number;
success_rate: number;
average_yield_percentage: number;
average_quality_score: number;
total_production_cost: number;
status_breakdown: Array<{
status: string;
count: number;
}>;
}
export class RecipesService {
private baseUrl = '/api/recipes/v1';
// Recipe Management
async getRecipes(tenantId: string, params?: RecipeSearchParams): Promise<Recipe[]> {
const response = await apiClient.get<Recipe[]>(`${this.baseUrl}/recipes`, {
headers: { 'X-Tenant-ID': tenantId },
params
});
return response.data;
}
async getRecipe(tenantId: string, recipeId: string): Promise<Recipe> {
const response = await apiClient.get<Recipe>(`${this.baseUrl}/recipes/${recipeId}`, {
headers: { 'X-Tenant-ID': tenantId }
});
return response.data;
}
async createRecipe(tenantId: string, userId: string, data: CreateRecipeRequest): Promise<Recipe> {
const response = await apiClient.post<Recipe>(`${this.baseUrl}/recipes`, data, {
headers: {
'X-Tenant-ID': tenantId,
'X-User-ID': userId
}
});
return response.data;
}
async updateRecipe(tenantId: string, userId: string, recipeId: string, data: UpdateRecipeRequest): Promise<Recipe> {
const response = await apiClient.put<Recipe>(`${this.baseUrl}/recipes/${recipeId}`, data, {
headers: {
'X-Tenant-ID': tenantId,
'X-User-ID': userId
}
});
return response.data;
}
async deleteRecipe(tenantId: string, recipeId: string): Promise<void> {
await apiClient.delete(`${this.baseUrl}/recipes/${recipeId}`, {
headers: { 'X-Tenant-ID': tenantId }
});
}
async duplicateRecipe(tenantId: string, userId: string, recipeId: string, newName: string): Promise<Recipe> {
const response = await apiClient.post<Recipe>(`${this.baseUrl}/recipes/${recipeId}/duplicate`,
{ new_name: newName },
{
headers: {
'X-Tenant-ID': tenantId,
'X-User-ID': userId
}
}
);
return response.data;
}
async activateRecipe(tenantId: string, userId: string, recipeId: string): Promise<Recipe> {
const response = await apiClient.post<Recipe>(`${this.baseUrl}/recipes/${recipeId}/activate`, {}, {
headers: {
'X-Tenant-ID': tenantId,
'X-User-ID': userId
}
});
return response.data;
}
async checkRecipeFeasibility(tenantId: string, recipeId: string, batchMultiplier: number = 1.0): Promise<RecipeFeasibility> {
const response = await apiClient.get<RecipeFeasibility>(`${this.baseUrl}/recipes/${recipeId}/feasibility`, {
headers: { 'X-Tenant-ID': tenantId },
params: { batch_multiplier: batchMultiplier }
});
return response.data;
}
async getRecipeStatistics(tenantId: string): Promise<RecipeStatistics> {
const response = await apiClient.get<RecipeStatistics>(`${this.baseUrl}/recipes/statistics/dashboard`, {
headers: { 'X-Tenant-ID': tenantId }
});
return response.data;
}
async getRecipeCategories(tenantId: string): Promise<string[]> {
const response = await apiClient.get<{ categories: string[] }>(`${this.baseUrl}/recipes/categories/list`, {
headers: { 'X-Tenant-ID': tenantId }
});
return response.data.categories;
}
// Production Management
async getProductionBatches(tenantId: string, params?: ProductionBatchSearchParams): Promise<ProductionBatch[]> {
const response = await apiClient.get<ProductionBatch[]>(`${this.baseUrl}/production/batches`, {
headers: { 'X-Tenant-ID': tenantId },
params
});
return response.data;
}
async getProductionBatch(tenantId: string, batchId: string): Promise<ProductionBatch> {
const response = await apiClient.get<ProductionBatch>(`${this.baseUrl}/production/batches/${batchId}`, {
headers: { 'X-Tenant-ID': tenantId }
});
return response.data;
}
async createProductionBatch(tenantId: string, userId: string, data: CreateProductionBatchRequest): Promise<ProductionBatch> {
const response = await apiClient.post<ProductionBatch>(`${this.baseUrl}/production/batches`, data, {
headers: {
'X-Tenant-ID': tenantId,
'X-User-ID': userId
}
});
return response.data;
}
async updateProductionBatch(tenantId: string, userId: string, batchId: string, data: UpdateProductionBatchRequest): Promise<ProductionBatch> {
const response = await apiClient.put<ProductionBatch>(`${this.baseUrl}/production/batches/${batchId}`, data, {
headers: {
'X-Tenant-ID': tenantId,
'X-User-ID': userId
}
});
return response.data;
}
async deleteProductionBatch(tenantId: string, batchId: string): Promise<void> {
await apiClient.delete(`${this.baseUrl}/production/batches/${batchId}`, {
headers: { 'X-Tenant-ID': tenantId }
});
}
async getActiveProductionBatches(tenantId: string): Promise<ProductionBatch[]> {
const response = await apiClient.get<ProductionBatch[]>(`${this.baseUrl}/production/batches/active/list`, {
headers: { 'X-Tenant-ID': tenantId }
});
return response.data;
}
async startProductionBatch(tenantId: string, userId: string, batchId: string, data: {
staff_member?: string;
production_notes?: string;
ingredient_consumptions: Array<{
recipe_ingredient_id: string;
ingredient_id: string;
stock_id?: string;
planned_quantity: number;
actual_quantity: number;
unit: string;
consumption_notes?: string;
ingredient_condition?: string;
substitution_used?: boolean;
substitution_details?: string;
}>;
}): Promise<ProductionBatch> {
const response = await apiClient.post<ProductionBatch>(`${this.baseUrl}/production/batches/${batchId}/start`, data, {
headers: {
'X-Tenant-ID': tenantId,
'X-User-ID': userId
}
});
return response.data;
}
async completeProductionBatch(tenantId: string, userId: string, batchId: string, data: {
actual_quantity: number;
quality_score?: number;
quality_notes?: string;
defect_rate?: number;
waste_quantity?: number;
waste_reason?: string;
production_notes?: string;
staff_member?: string;
}): Promise<ProductionBatch> {
const response = await apiClient.post<ProductionBatch>(`${this.baseUrl}/production/batches/${batchId}/complete`, data, {
headers: {
'X-Tenant-ID': tenantId,
'X-User-ID': userId
}
});
return response.data;
}
async getProductionStatistics(tenantId: string, startDate?: string, endDate?: string): Promise<ProductionStatistics> {
const response = await apiClient.get<ProductionStatistics>(`${this.baseUrl}/production/statistics/dashboard`, {
headers: { 'X-Tenant-ID': tenantId },
params: { start_date: startDate, end_date: endDate }
});
return response.data;
}
}

View File

@@ -0,0 +1,622 @@
// frontend/src/api/services/suppliers.service.ts
/**
* Supplier & Procurement API Service
* Handles all communication with the supplier service backend
*/
import { apiClient } from '../client';
// ============================================================================
// TYPES & INTERFACES
// ============================================================================
export interface Supplier {
id: string;
tenant_id: string;
name: string;
supplier_code?: string;
tax_id?: string;
registration_number?: string;
supplier_type: 'INGREDIENTS' | 'PACKAGING' | 'EQUIPMENT' | 'SERVICES' | 'UTILITIES' | 'MULTI';
status: 'ACTIVE' | 'INACTIVE' | 'PENDING_APPROVAL' | 'SUSPENDED' | 'BLACKLISTED';
contact_person?: string;
email?: string;
phone?: string;
mobile?: string;
website?: string;
// Address
address_line1?: string;
address_line2?: string;
city?: string;
state_province?: string;
postal_code?: string;
country?: string;
// Business terms
payment_terms: 'CASH_ON_DELIVERY' | 'NET_15' | 'NET_30' | 'NET_45' | 'NET_60' | 'PREPAID' | 'CREDIT_TERMS';
credit_limit?: number;
currency: string;
standard_lead_time: number;
minimum_order_amount?: number;
delivery_area?: string;
// Performance metrics
quality_rating?: number;
delivery_rating?: number;
total_orders: number;
total_amount: number;
// Approval info
approved_by?: string;
approved_at?: string;
rejection_reason?: string;
// Additional information
notes?: string;
certifications?: Record<string, any>;
business_hours?: Record<string, any>;
specializations?: Record<string, any>;
// Audit fields
created_at: string;
updated_at: string;
created_by: string;
updated_by: string;
}
export interface SupplierSummary {
id: string;
name: string;
supplier_code?: string;
supplier_type: string;
status: string;
contact_person?: string;
email?: string;
phone?: string;
city?: string;
country?: string;
quality_rating?: number;
delivery_rating?: number;
total_orders: number;
total_amount: number;
created_at: string;
}
export interface CreateSupplierRequest {
name: string;
supplier_code?: string;
tax_id?: string;
registration_number?: string;
supplier_type: string;
contact_person?: string;
email?: string;
phone?: string;
mobile?: string;
website?: string;
address_line1?: string;
address_line2?: string;
city?: string;
state_province?: string;
postal_code?: string;
country?: string;
payment_terms?: string;
credit_limit?: number;
currency?: string;
standard_lead_time?: number;
minimum_order_amount?: number;
delivery_area?: string;
notes?: string;
certifications?: Record<string, any>;
business_hours?: Record<string, any>;
specializations?: Record<string, any>;
}
export interface UpdateSupplierRequest extends Partial<CreateSupplierRequest> {
status?: string;
}
export interface PurchaseOrder {
id: string;
tenant_id: string;
supplier_id: string;
po_number: string;
reference_number?: string;
status: 'DRAFT' | 'PENDING_APPROVAL' | 'APPROVED' | 'SENT_TO_SUPPLIER' | 'CONFIRMED' | 'PARTIALLY_RECEIVED' | 'COMPLETED' | 'CANCELLED' | 'DISPUTED';
priority: string;
order_date: string;
required_delivery_date?: string;
estimated_delivery_date?: string;
// Financial information
subtotal: number;
tax_amount: number;
shipping_cost: number;
discount_amount: number;
total_amount: number;
currency: string;
// Delivery information
delivery_address?: string;
delivery_instructions?: string;
delivery_contact?: string;
delivery_phone?: string;
// Approval workflow
requires_approval: boolean;
approved_by?: string;
approved_at?: string;
rejection_reason?: string;
// Communication tracking
sent_to_supplier_at?: string;
supplier_confirmation_date?: string;
supplier_reference?: string;
// Additional information
notes?: string;
internal_notes?: string;
terms_and_conditions?: string;
// Audit fields
created_at: string;
updated_at: string;
created_by: string;
updated_by: string;
// Related data
supplier?: SupplierSummary;
items?: PurchaseOrderItem[];
}
export interface PurchaseOrderItem {
id: string;
tenant_id: string;
purchase_order_id: string;
price_list_item_id?: string;
ingredient_id: string;
product_code?: string;
product_name: string;
ordered_quantity: number;
unit_of_measure: string;
unit_price: number;
line_total: number;
received_quantity: number;
remaining_quantity: number;
quality_requirements?: string;
item_notes?: string;
created_at: string;
updated_at: string;
}
export interface CreatePurchaseOrderRequest {
supplier_id: string;
reference_number?: string;
priority?: string;
required_delivery_date?: string;
delivery_address?: string;
delivery_instructions?: string;
delivery_contact?: string;
delivery_phone?: string;
tax_amount?: number;
shipping_cost?: number;
discount_amount?: number;
notes?: string;
internal_notes?: string;
terms_and_conditions?: string;
items: {
ingredient_id: string;
product_code?: string;
product_name: string;
ordered_quantity: number;
unit_of_measure: string;
unit_price: number;
quality_requirements?: string;
item_notes?: string;
}[];
}
export interface Delivery {
id: string;
tenant_id: string;
purchase_order_id: string;
supplier_id: string;
delivery_number: string;
supplier_delivery_note?: string;
status: 'SCHEDULED' | 'IN_TRANSIT' | 'OUT_FOR_DELIVERY' | 'DELIVERED' | 'PARTIALLY_DELIVERED' | 'FAILED_DELIVERY' | 'RETURNED';
// Timing
scheduled_date?: string;
estimated_arrival?: string;
actual_arrival?: string;
completed_at?: string;
// Delivery details
delivery_address?: string;
delivery_contact?: string;
delivery_phone?: string;
carrier_name?: string;
tracking_number?: string;
// Quality inspection
inspection_passed?: boolean;
inspection_notes?: string;
quality_issues?: Record<string, any>;
// Receipt information
received_by?: string;
received_at?: string;
// Additional information
notes?: string;
photos?: Record<string, any>;
// Audit fields
created_at: string;
updated_at: string;
created_by: string;
// Related data
supplier?: SupplierSummary;
purchase_order?: { id: string; po_number: string };
items?: DeliveryItem[];
}
export interface DeliveryItem {
id: string;
tenant_id: string;
delivery_id: string;
purchase_order_item_id: string;
ingredient_id: string;
product_name: string;
ordered_quantity: number;
delivered_quantity: number;
accepted_quantity: number;
rejected_quantity: number;
batch_lot_number?: string;
expiry_date?: string;
quality_grade?: string;
quality_issues?: string;
rejection_reason?: string;
item_notes?: string;
created_at: string;
updated_at: string;
}
export interface SupplierSearchParams {
search_term?: string;
supplier_type?: string;
status?: string;
limit?: number;
offset?: number;
}
export interface PurchaseOrderSearchParams {
supplier_id?: string;
status?: string;
priority?: string;
date_from?: string;
date_to?: string;
search_term?: string;
limit?: number;
offset?: number;
}
export interface DeliverySearchParams {
supplier_id?: string;
status?: string;
date_from?: string;
date_to?: string;
search_term?: string;
limit?: number;
offset?: number;
}
export interface SupplierStatistics {
total_suppliers: number;
active_suppliers: number;
pending_suppliers: number;
avg_quality_rating: number;
avg_delivery_rating: number;
total_spend: number;
}
export interface PurchaseOrderStatistics {
total_orders: number;
status_counts: Record<string, number>;
this_month_orders: number;
this_month_spend: number;
avg_order_value: number;
overdue_count: number;
pending_approval: number;
}
export interface DeliveryPerformanceStats {
total_deliveries: number;
on_time_deliveries: number;
late_deliveries: number;
failed_deliveries: number;
on_time_percentage: number;
avg_delay_hours: number;
quality_pass_rate: number;
}
// ============================================================================
// SUPPLIERS SERVICE CLASS
// ============================================================================
export class SuppliersService {
private baseUrl = '/api/v1/suppliers';
// Suppliers CRUD Operations
async getSuppliers(tenantId: string, params?: SupplierSearchParams): Promise<SupplierSummary[]> {
const searchParams = new URLSearchParams();
if (params?.search_term) searchParams.append('search_term', params.search_term);
if (params?.supplier_type) searchParams.append('supplier_type', params.supplier_type);
if (params?.status) searchParams.append('status', params.status);
if (params?.limit) searchParams.append('limit', params.limit.toString());
if (params?.offset) searchParams.append('offset', params.offset.toString());
const response = await apiClient.get<SupplierSummary[]>(
`${this.baseUrl}?${searchParams.toString()}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
async getSupplier(tenantId: string, supplierId: string): Promise<Supplier> {
const response = await apiClient.get<Supplier>(
`${this.baseUrl}/${supplierId}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
async createSupplier(tenantId: string, userId: string, data: CreateSupplierRequest): Promise<Supplier> {
const response = await apiClient.post<Supplier>(
this.baseUrl,
data,
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
}
async updateSupplier(tenantId: string, userId: string, supplierId: string, data: UpdateSupplierRequest): Promise<Supplier> {
const response = await apiClient.put<Supplier>(
`${this.baseUrl}/${supplierId}`,
data,
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
}
async deleteSupplier(tenantId: string, supplierId: string): Promise<void> {
await apiClient.delete(
`${this.baseUrl}/${supplierId}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
}
async approveSupplier(tenantId: string, userId: string, supplierId: string, action: 'approve' | 'reject', notes?: string): Promise<Supplier> {
const response = await apiClient.post<Supplier>(
`${this.baseUrl}/${supplierId}/approve`,
{ action, notes },
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
}
// Supplier Analytics & Lists
async getSupplierStatistics(tenantId: string): Promise<SupplierStatistics> {
const response = await apiClient.get<SupplierStatistics>(
`${this.baseUrl}/statistics`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
async getActiveSuppliers(tenantId: string): Promise<SupplierSummary[]> {
const response = await apiClient.get<SupplierSummary[]>(
`${this.baseUrl}/active`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
async getTopSuppliers(tenantId: string, limit: number = 10): Promise<SupplierSummary[]> {
const response = await apiClient.get<SupplierSummary[]>(
`${this.baseUrl}/top?limit=${limit}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
async getSuppliersByType(tenantId: string, supplierType: string): Promise<SupplierSummary[]> {
const response = await apiClient.get<SupplierSummary[]>(
`${this.baseUrl}/types/${supplierType}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
async getSuppliersNeedingReview(tenantId: string, daysSinceLastOrder: number = 30): Promise<SupplierSummary[]> {
const response = await apiClient.get<SupplierSummary[]>(
`${this.baseUrl}/pending-review?days_since_last_order=${daysSinceLastOrder}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
// Purchase Orders
async getPurchaseOrders(tenantId: string, params?: PurchaseOrderSearchParams): Promise<PurchaseOrder[]> {
const searchParams = new URLSearchParams();
if (params?.supplier_id) searchParams.append('supplier_id', params.supplier_id);
if (params?.status) searchParams.append('status', params.status);
if (params?.priority) searchParams.append('priority', params.priority);
if (params?.date_from) searchParams.append('date_from', params.date_from);
if (params?.date_to) searchParams.append('date_to', params.date_to);
if (params?.search_term) searchParams.append('search_term', params.search_term);
if (params?.limit) searchParams.append('limit', params.limit.toString());
if (params?.offset) searchParams.append('offset', params.offset.toString());
const response = await apiClient.get<PurchaseOrder[]>(
`/api/v1/purchase-orders?${searchParams.toString()}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
async getPurchaseOrder(tenantId: string, poId: string): Promise<PurchaseOrder> {
const response = await apiClient.get<PurchaseOrder>(
`/api/v1/purchase-orders/${poId}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
async createPurchaseOrder(tenantId: string, userId: string, data: CreatePurchaseOrderRequest): Promise<PurchaseOrder> {
const response = await apiClient.post<PurchaseOrder>(
'/api/v1/purchase-orders',
data,
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
}
async updatePurchaseOrderStatus(tenantId: string, userId: string, poId: string, status: string, notes?: string): Promise<PurchaseOrder> {
const response = await apiClient.patch<PurchaseOrder>(
`/api/v1/purchase-orders/${poId}/status`,
{ status, notes },
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
}
async approvePurchaseOrder(tenantId: string, userId: string, poId: string, action: 'approve' | 'reject', notes?: string): Promise<PurchaseOrder> {
const response = await apiClient.post<PurchaseOrder>(
`/api/v1/purchase-orders/${poId}/approve`,
{ action, notes },
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
}
async sendToSupplier(tenantId: string, userId: string, poId: string, sendEmail: boolean = true): Promise<PurchaseOrder> {
const response = await apiClient.post<PurchaseOrder>(
`/api/v1/purchase-orders/${poId}/send-to-supplier?send_email=${sendEmail}`,
{},
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
}
async cancelPurchaseOrder(tenantId: string, userId: string, poId: string, reason: string): Promise<PurchaseOrder> {
const response = await apiClient.post<PurchaseOrder>(
`/api/v1/purchase-orders/${poId}/cancel?cancellation_reason=${encodeURIComponent(reason)}`,
{},
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
}
async getPurchaseOrderStatistics(tenantId: string): Promise<PurchaseOrderStatistics> {
const response = await apiClient.get<PurchaseOrderStatistics>(
'/api/v1/purchase-orders/statistics',
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
async getOrdersRequiringApproval(tenantId: string): Promise<PurchaseOrder[]> {
const response = await apiClient.get<PurchaseOrder[]>(
'/api/v1/purchase-orders/pending-approval',
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
async getOverdueOrders(tenantId: string): Promise<PurchaseOrder[]> {
const response = await apiClient.get<PurchaseOrder[]>(
'/api/v1/purchase-orders/overdue',
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
// Deliveries
async getDeliveries(tenantId: string, params?: DeliverySearchParams): Promise<Delivery[]> {
const searchParams = new URLSearchParams();
if (params?.supplier_id) searchParams.append('supplier_id', params.supplier_id);
if (params?.status) searchParams.append('status', params.status);
if (params?.date_from) searchParams.append('date_from', params.date_from);
if (params?.date_to) searchParams.append('date_to', params.date_to);
if (params?.search_term) searchParams.append('search_term', params.search_term);
if (params?.limit) searchParams.append('limit', params.limit.toString());
if (params?.offset) searchParams.append('offset', params.offset.toString());
const response = await apiClient.get<Delivery[]>(
`/api/v1/deliveries?${searchParams.toString()}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
async getDelivery(tenantId: string, deliveryId: string): Promise<Delivery> {
const response = await apiClient.get<Delivery>(
`/api/v1/deliveries/${deliveryId}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
async getTodaysDeliveries(tenantId: string): Promise<Delivery[]> {
const response = await apiClient.get<Delivery[]>(
'/api/v1/deliveries/today',
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
async getOverdueDeliveries(tenantId: string): Promise<Delivery[]> {
const response = await apiClient.get<Delivery[]>(
'/api/v1/deliveries/overdue',
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
async updateDeliveryStatus(tenantId: string, userId: string, deliveryId: string, status: string, notes?: string): Promise<Delivery> {
const response = await apiClient.patch<Delivery>(
`/api/v1/deliveries/${deliveryId}/status`,
{ status, notes, update_timestamps: true },
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
}
async receiveDelivery(tenantId: string, userId: string, deliveryId: string, receiptData: {
inspection_passed?: boolean;
inspection_notes?: string;
quality_issues?: Record<string, any>;
notes?: string;
}): Promise<Delivery> {
const response = await apiClient.post<Delivery>(
`/api/v1/deliveries/${deliveryId}/receive`,
receiptData,
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
}
async getDeliveryPerformanceStats(tenantId: string, daysBack: number = 30, supplierId?: string): Promise<DeliveryPerformanceStats> {
const params = new URLSearchParams();
params.append('days_back', daysBack.toString());
if (supplierId) params.append('supplier_id', supplierId);
const response = await apiClient.get<DeliveryPerformanceStats>(
`/api/v1/deliveries/performance-stats?${params.toString()}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
}
}