Create new services: inventory, recipes, suppliers
This commit is contained in:
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();
|
||||
Reference in New Issue
Block a user