Create new services: inventory, recipes, suppliers
This commit is contained in:
@@ -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
|
||||
|
||||
474
frontend/src/api/services/inventory.service.ts
Normal file
474
frontend/src/api/services/inventory.service.ts
Normal 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();
|
||||
@@ -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();
|
||||
551
frontend/src/api/services/recipes.service.ts
Normal file
551
frontend/src/api/services/recipes.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
622
frontend/src/api/services/suppliers.service.ts
Normal file
622
frontend/src/api/services/suppliers.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user