749 lines
22 KiB
TypeScript
749 lines
22 KiB
TypeScript
// frontend/src/api/services/inventory.service.ts
|
|
/**
|
|
* Inventory Service
|
|
* Handles inventory management, stock tracking, and product operations
|
|
*/
|
|
|
|
import { apiClient } from '../client';
|
|
import type { ProductInfo } from '../types';
|
|
|
|
// ========== 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;
|
|
sku?: 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;
|
|
}
|
|
|
|
|
|
// ========== 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 = '';
|
|
|
|
// ========== 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 = `/tenants/${tenantId}/ingredients${query ? `?${query}` : ''}`;
|
|
|
|
console.log('🔍 InventoryService: Fetching inventory items from:', url);
|
|
|
|
try {
|
|
console.log('🔑 InventoryService: Making request with auth token:', localStorage.getItem('auth_token') ? 'Present' : 'Missing');
|
|
const response = await apiClient.get(url);
|
|
console.log('📋 InventoryService: Raw response:', response);
|
|
console.log('📋 InventoryService: Response type:', typeof response);
|
|
console.log('📋 InventoryService: Response keys:', response ? Object.keys(response) : 'null');
|
|
|
|
// Handle different response formats
|
|
if (Array.isArray(response)) {
|
|
// Direct array response
|
|
console.log('✅ InventoryService: Array response with', response.length, 'items');
|
|
return {
|
|
items: response,
|
|
total: response.length,
|
|
page: 1,
|
|
limit: response.length,
|
|
total_pages: 1
|
|
};
|
|
} else if (response && typeof response === 'object') {
|
|
// Check if it's already paginated
|
|
if ('items' in response && Array.isArray(response.items)) {
|
|
console.log('✅ InventoryService: Paginated response with', response.items.length, 'items');
|
|
return response;
|
|
}
|
|
|
|
// Handle object with numeric keys (convert to array)
|
|
const keys = Object.keys(response);
|
|
if (keys.length > 0 && keys.every(key => !isNaN(Number(key)))) {
|
|
const items = Object.values(response);
|
|
console.log('✅ InventoryService: Numeric keys response with', items.length, 'items');
|
|
return {
|
|
items,
|
|
total: items.length,
|
|
page: 1,
|
|
limit: items.length,
|
|
total_pages: 1
|
|
};
|
|
}
|
|
|
|
// Handle empty object - this seems to be what we're getting
|
|
if (keys.length === 0) {
|
|
console.log('📭 InventoryService: Empty object response - backend has no inventory items for this tenant');
|
|
throw new Error('NO_INVENTORY_ITEMS'); // This will trigger fallback in useInventory
|
|
}
|
|
}
|
|
|
|
// Fallback: unexpected response format
|
|
console.warn('⚠️ InventoryService: Unexpected response format, keys:', Object.keys(response || {}));
|
|
throw new Error('UNEXPECTED_RESPONSE_FORMAT');
|
|
} catch (error) {
|
|
console.error('❌ InventoryService: Failed to fetch inventory items:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get single inventory item by ID
|
|
*/
|
|
async getInventoryItem(tenantId: string, itemId: string): Promise<InventoryItem> {
|
|
return apiClient.get(`/tenants/${tenantId}/ingredients/${itemId}`);
|
|
}
|
|
|
|
/**
|
|
* Create new inventory item
|
|
*/
|
|
async createInventoryItem(
|
|
tenantId: string,
|
|
data: CreateInventoryItemRequest
|
|
): Promise<InventoryItem> {
|
|
return apiClient.post(`/tenants/${tenantId}/ingredients`, data);
|
|
}
|
|
|
|
/**
|
|
* Update existing inventory item
|
|
*/
|
|
async updateInventoryItem(
|
|
tenantId: string,
|
|
itemId: string,
|
|
data: UpdateInventoryItemRequest
|
|
): Promise<InventoryItem> {
|
|
return apiClient.put(`/tenants/${tenantId}/ingredients/${itemId}`, data);
|
|
}
|
|
|
|
/**
|
|
* Delete inventory item (soft delete)
|
|
*/
|
|
async deleteInventoryItem(tenantId: string, itemId: string): Promise<void> {
|
|
return apiClient.delete(`/tenants/${tenantId}/ingredients/${itemId}`);
|
|
}
|
|
|
|
/**
|
|
* Bulk update inventory items
|
|
*/
|
|
async bulkUpdateInventoryItems(
|
|
tenantId: string,
|
|
updates: { id: string; data: UpdateInventoryItemRequest }[]
|
|
): Promise<{ success: number; failed: number; errors: string[] }> {
|
|
return apiClient.post(`/tenants/${tenantId}/ingredients/bulk-update`, {
|
|
updates
|
|
});
|
|
}
|
|
|
|
// ========== STOCK MANAGEMENT ==========
|
|
|
|
/**
|
|
* Get current stock level for an item
|
|
*/
|
|
async getStockLevel(tenantId: string, itemId: string): Promise<StockLevel> {
|
|
return apiClient.get(`/tenants/${tenantId}/ingredients/${itemId}/stock`);
|
|
}
|
|
|
|
/**
|
|
* Get stock levels for all items
|
|
*/
|
|
async getAllStockLevels(tenantId: string): Promise<StockLevel[]> {
|
|
// TODO: Map to correct endpoint when available
|
|
return [];
|
|
// return apiClient.get(`/stock/summary`);
|
|
}
|
|
|
|
/**
|
|
* Adjust stock level (purchase, consumption, waste, etc.)
|
|
*/
|
|
async adjustStock(
|
|
tenantId: string,
|
|
itemId: string,
|
|
adjustment: StockAdjustmentRequest
|
|
): Promise<StockMovement> {
|
|
return apiClient.post(
|
|
`/stock/consume`,
|
|
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);
|
|
}
|
|
|
|
|
|
// ========== DASHBOARD & ANALYTICS ==========
|
|
|
|
|
|
/**
|
|
* 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`);
|
|
}
|
|
|
|
// ========== PRODUCTS FOR FORECASTING ==========
|
|
|
|
/**
|
|
* Get Products List with IDs for Forecasting
|
|
*/
|
|
async getProductsList(tenantId: string): Promise<ProductInfo[]> {
|
|
try {
|
|
console.log('🔍 Fetching products for forecasting...', { tenantId });
|
|
|
|
// First try to get finished products (preferred for forecasting)
|
|
const response = await apiClient.get(`/tenants/${tenantId}/ingredients`, {
|
|
params: {
|
|
limit: 100,
|
|
product_type: 'finished_product'
|
|
},
|
|
});
|
|
|
|
console.log('🔍 Inventory Products API Response:', response);
|
|
console.log('🔍 Raw response data:', response.data);
|
|
console.log('🔍 Response status:', response.status);
|
|
console.log('🔍 Response headers:', response.headers);
|
|
console.log('🔍 Full response object keys:', Object.keys(response || {}));
|
|
console.log('🔍 Response data type:', typeof response);
|
|
console.log('🔍 Response data constructor:', response?.constructor?.name);
|
|
|
|
// Check if response.data exists and what type it is
|
|
if (response && 'data' in response) {
|
|
console.log('🔍 Response.data exists:', typeof response.data);
|
|
console.log('🔍 Response.data keys:', Object.keys(response.data || {}));
|
|
console.log('🔍 Response.data constructor:', response.data?.constructor?.name);
|
|
}
|
|
|
|
let productsArray: any[] = [];
|
|
|
|
// Check response.data first (typical API client behavior)
|
|
const dataToProcess = response?.data || response;
|
|
|
|
if (Array.isArray(dataToProcess)) {
|
|
productsArray = dataToProcess;
|
|
console.log('✅ Found array data with', productsArray.length, 'items');
|
|
} else if (dataToProcess && typeof dataToProcess === 'object') {
|
|
// Handle different response formats
|
|
const keys = Object.keys(dataToProcess);
|
|
if (keys.length > 0 && keys.every(key => !isNaN(Number(key)))) {
|
|
productsArray = Object.values(dataToProcess);
|
|
console.log('✅ Found object with numeric keys, converted to array with', productsArray.length, 'items');
|
|
} else {
|
|
console.warn('⚠️ Response is object but not with numeric keys:', dataToProcess);
|
|
console.warn('⚠️ Object keys:', keys);
|
|
return [];
|
|
}
|
|
} else {
|
|
console.warn('⚠️ Response data is not array or object:', dataToProcess);
|
|
return [];
|
|
}
|
|
|
|
// Convert to ProductInfo objects
|
|
const products: ProductInfo[] = productsArray
|
|
.map((product: any) => ({
|
|
inventory_product_id: product.id || product.inventory_product_id,
|
|
name: product.name || product.product_name || `Product ${product.id || ''}`,
|
|
category: product.category,
|
|
// Add additional fields if available from inventory
|
|
current_stock: product.current_stock,
|
|
unit: product.unit,
|
|
cost_per_unit: product.cost_per_unit
|
|
}))
|
|
.filter(product => product.inventory_product_id && product.name);
|
|
|
|
console.log('📋 Processed finished products:', products);
|
|
|
|
// If no finished products found, try to get all products as fallback
|
|
if (products.length === 0) {
|
|
console.log('⚠️ No finished products found, trying to get all products as fallback...');
|
|
|
|
const fallbackResponse = await apiClient.get(`/tenants/${tenantId}/ingredients`, {
|
|
params: {
|
|
limit: 100,
|
|
// No product_type filter to get all products
|
|
},
|
|
});
|
|
|
|
console.log('🔍 Fallback API Response:', fallbackResponse);
|
|
|
|
const fallbackDataToProcess = fallbackResponse?.data || fallbackResponse;
|
|
let fallbackProductsArray: any[] = [];
|
|
|
|
if (Array.isArray(fallbackDataToProcess)) {
|
|
fallbackProductsArray = fallbackDataToProcess;
|
|
} else if (fallbackDataToProcess && typeof fallbackDataToProcess === 'object') {
|
|
const keys = Object.keys(fallbackDataToProcess);
|
|
if (keys.length > 0 && keys.every(key => !isNaN(Number(key)))) {
|
|
fallbackProductsArray = Object.values(fallbackDataToProcess);
|
|
}
|
|
}
|
|
|
|
const fallbackProducts: ProductInfo[] = fallbackProductsArray
|
|
.map((product: any) => ({
|
|
inventory_product_id: product.id || product.inventory_product_id,
|
|
name: product.name || product.product_name || `Product ${product.id || ''}`,
|
|
category: product.category,
|
|
current_stock: product.current_stock,
|
|
unit: product.unit,
|
|
cost_per_unit: product.cost_per_unit
|
|
}))
|
|
.filter(product => product.inventory_product_id && product.name);
|
|
|
|
console.log('📋 Processed fallback products (all inventory items):', fallbackProducts);
|
|
return fallbackProducts;
|
|
}
|
|
|
|
return products;
|
|
|
|
} catch (error) {
|
|
console.error('❌ Failed to fetch inventory products:', error);
|
|
console.error('❌ Error details:', {
|
|
message: error instanceof Error ? error.message : 'Unknown error',
|
|
response: (error as any)?.response,
|
|
status: (error as any)?.response?.status,
|
|
data: (error as any)?.response?.data
|
|
});
|
|
|
|
// If it's an authentication error, throw it to trigger auth flow
|
|
if ((error as any)?.response?.status === 401) {
|
|
throw error;
|
|
}
|
|
|
|
// Return empty array on other errors - let dashboard handle fallback
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get Product by ID
|
|
*/
|
|
async getProductById(tenantId: string, productId: string): Promise<ProductInfo | null> {
|
|
try {
|
|
const response = await apiClient.get(`/tenants/${tenantId}/ingredients/${productId}`);
|
|
|
|
if (response) {
|
|
return {
|
|
inventory_product_id: response.id || response.inventory_product_id,
|
|
name: response.name || response.product_name,
|
|
category: response.category,
|
|
current_stock: response.current_stock,
|
|
unit: response.unit,
|
|
cost_per_unit: response.cost_per_unit
|
|
};
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
console.error('❌ Failed to fetch product by ID:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ========== ENHANCED DASHBOARD FEATURES ==========
|
|
|
|
/**
|
|
* Get inventory dashboard data with analytics
|
|
*/
|
|
async getDashboardData(tenantId: string, params?: {
|
|
date_from?: string;
|
|
date_to?: string;
|
|
location?: string;
|
|
}): Promise<{
|
|
summary: {
|
|
total_items: number;
|
|
low_stock_count: number;
|
|
out_of_stock_items: number;
|
|
expiring_soon: number;
|
|
total_value: number;
|
|
};
|
|
recent_movements: any[];
|
|
active_alerts: any[];
|
|
stock_trends: {
|
|
dates: string[];
|
|
stock_levels: number[];
|
|
movements_in: number[];
|
|
movements_out: number[];
|
|
};
|
|
}> {
|
|
try {
|
|
return await apiClient.get(`/tenants/${tenantId}/inventory/dashboard`, { params });
|
|
} catch (error) {
|
|
console.error('❌ Error fetching inventory dashboard:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get food safety compliance data
|
|
*/
|
|
async getFoodSafetyCompliance(tenantId: string): Promise<{
|
|
compliant_items: number;
|
|
non_compliant_items: number;
|
|
expiring_items: any[];
|
|
temperature_violations: any[];
|
|
compliance_score: number;
|
|
}> {
|
|
try {
|
|
return await apiClient.get(`/tenants/${tenantId}/inventory/food-safety/compliance`);
|
|
} catch (error) {
|
|
console.error('❌ Error fetching food safety compliance:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get temperature monitoring data
|
|
*/
|
|
async getTemperatureMonitoring(tenantId: string, params?: {
|
|
item_id?: string;
|
|
location?: string;
|
|
date_from?: string;
|
|
date_to?: string;
|
|
}): Promise<{
|
|
readings: any[];
|
|
violations: any[];
|
|
}> {
|
|
try {
|
|
return await apiClient.get(`/tenants/${tenantId}/inventory/food-safety/temperature-monitoring`, { params });
|
|
} catch (error) {
|
|
console.error('❌ Error fetching temperature monitoring:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Record temperature reading
|
|
*/
|
|
async recordTemperatureReading(tenantId: string, params: {
|
|
item_id: string;
|
|
temperature: number;
|
|
humidity?: number;
|
|
location: string;
|
|
notes?: string;
|
|
}): Promise<void> {
|
|
try {
|
|
return await apiClient.post(`/tenants/${tenantId}/inventory/food-safety/temperature-reading`, params);
|
|
} catch (error) {
|
|
console.error('❌ Error recording temperature reading:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Get restock recommendations
|
|
*/
|
|
async getRestockRecommendations(tenantId: string): Promise<{
|
|
urgent_restocks: any[];
|
|
optimal_orders: any[];
|
|
}> {
|
|
try {
|
|
return await apiClient.get(`/tenants/${tenantId}/inventory/forecasting/restock-recommendations`);
|
|
} catch (error) {
|
|
console.error('❌ Error fetching restock recommendations:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const inventoryService = new InventoryService(); |