Create new services: inventory, recipes, suppliers
This commit is contained in:
@@ -11,6 +11,8 @@ export { useTraining } from './useTraining';
|
||||
export { useForecast } from './useForecast';
|
||||
export { useNotification } from './useNotification';
|
||||
export { useOnboarding, useOnboardingStep } from './useOnboarding';
|
||||
export { useInventory, useInventoryDashboard, useInventoryItem } from './useInventory';
|
||||
export { useRecipes, useProduction } from './useRecipes';
|
||||
|
||||
// Import hooks for combined usage
|
||||
import { useAuth } from './useAuth';
|
||||
|
||||
510
frontend/src/api/hooks/useInventory.ts
Normal file
510
frontend/src/api/hooks/useInventory.ts
Normal file
@@ -0,0 +1,510 @@
|
||||
// frontend/src/api/hooks/useInventory.ts
|
||||
/**
|
||||
* Inventory Management React Hook
|
||||
* Provides comprehensive state management for inventory operations
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import {
|
||||
inventoryService,
|
||||
InventoryItem,
|
||||
StockLevel,
|
||||
StockMovement,
|
||||
StockAlert,
|
||||
InventorySearchParams,
|
||||
CreateInventoryItemRequest,
|
||||
UpdateInventoryItemRequest,
|
||||
StockAdjustmentRequest,
|
||||
PaginatedResponse,
|
||||
InventoryDashboardData
|
||||
} from '../services/inventory.service';
|
||||
|
||||
import { useTenantId } from '../../hooks/useTenantId';
|
||||
|
||||
// ========== HOOK INTERFACES ==========
|
||||
|
||||
interface UseInventoryReturn {
|
||||
// State
|
||||
items: InventoryItem[];
|
||||
stockLevels: Record<string, StockLevel>;
|
||||
movements: StockMovement[];
|
||||
alerts: StockAlert[];
|
||||
dashboardData: InventoryDashboardData | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
|
||||
// Actions
|
||||
loadItems: (params?: InventorySearchParams) => Promise<void>;
|
||||
loadItem: (itemId: string) => Promise<InventoryItem | null>;
|
||||
createItem: (data: CreateInventoryItemRequest) => Promise<InventoryItem | null>;
|
||||
updateItem: (itemId: string, data: UpdateInventoryItemRequest) => Promise<InventoryItem | null>;
|
||||
deleteItem: (itemId: string) => Promise<boolean>;
|
||||
|
||||
// Stock operations
|
||||
loadStockLevels: () => Promise<void>;
|
||||
adjustStock: (itemId: string, adjustment: StockAdjustmentRequest) => Promise<StockMovement | null>;
|
||||
loadMovements: (params?: any) => Promise<void>;
|
||||
|
||||
// Alerts
|
||||
loadAlerts: () => Promise<void>;
|
||||
acknowledgeAlert: (alertId: string) => Promise<boolean>;
|
||||
|
||||
// Dashboard
|
||||
loadDashboard: () => Promise<void>;
|
||||
|
||||
// Utility
|
||||
searchItems: (query: string) => Promise<InventoryItem[]>;
|
||||
refresh: () => Promise<void>;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
interface UseInventoryDashboardReturn {
|
||||
dashboardData: InventoryDashboardData | null;
|
||||
alerts: StockAlert[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface UseInventoryItemReturn {
|
||||
item: InventoryItem | null;
|
||||
stockLevel: StockLevel | null;
|
||||
recentMovements: StockMovement[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
updateItem: (data: UpdateInventoryItemRequest) => Promise<boolean>;
|
||||
adjustStock: (adjustment: StockAdjustmentRequest) => Promise<boolean>;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
// ========== MAIN INVENTORY HOOK ==========
|
||||
|
||||
export const useInventory = (autoLoad = true): UseInventoryReturn => {
|
||||
const tenantId = useTenantId();
|
||||
|
||||
// State
|
||||
const [items, setItems] = useState<InventoryItem[]>([]);
|
||||
const [stockLevels, setStockLevels] = useState<Record<string, StockLevel>>({});
|
||||
const [movements, setMovements] = useState<StockMovement[]>([]);
|
||||
const [alerts, setAlerts] = useState<StockAlert[]>([]);
|
||||
const [dashboardData, setDashboardData] = useState<InventoryDashboardData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
});
|
||||
|
||||
// Clear error
|
||||
const clearError = useCallback(() => setError(null), []);
|
||||
|
||||
// Load inventory items
|
||||
const loadItems = useCallback(async (params?: InventorySearchParams) => {
|
||||
if (!tenantId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await inventoryService.getInventoryItems(tenantId, params);
|
||||
setItems(response.items);
|
||||
setPagination({
|
||||
page: response.page,
|
||||
limit: response.limit,
|
||||
total: response.total,
|
||||
totalPages: response.total_pages
|
||||
});
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading inventory items';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Load single item
|
||||
const loadItem = useCallback(async (itemId: string): Promise<InventoryItem | null> => {
|
||||
if (!tenantId) return null;
|
||||
|
||||
try {
|
||||
const item = await inventoryService.getInventoryItem(tenantId, itemId);
|
||||
|
||||
// Update in local state if it exists
|
||||
setItems(prev => prev.map(i => i.id === itemId ? item : i));
|
||||
|
||||
return item;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading item';
|
||||
setError(errorMessage);
|
||||
return null;
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Create item
|
||||
const createItem = useCallback(async (data: CreateInventoryItemRequest): Promise<InventoryItem | null> => {
|
||||
if (!tenantId) return null;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const newItem = await inventoryService.createInventoryItem(tenantId, data);
|
||||
setItems(prev => [newItem, ...prev]);
|
||||
toast.success(`Created ${newItem.name} successfully`);
|
||||
return newItem;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error creating item';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Update item
|
||||
const updateItem = useCallback(async (
|
||||
itemId: string,
|
||||
data: UpdateInventoryItemRequest
|
||||
): Promise<InventoryItem | null> => {
|
||||
if (!tenantId) return null;
|
||||
|
||||
try {
|
||||
const updatedItem = await inventoryService.updateInventoryItem(tenantId, itemId, data);
|
||||
setItems(prev => prev.map(i => i.id === itemId ? updatedItem : i));
|
||||
toast.success(`Updated ${updatedItem.name} successfully`);
|
||||
return updatedItem;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error updating item';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Delete item
|
||||
const deleteItem = useCallback(async (itemId: string): Promise<boolean> => {
|
||||
if (!tenantId) return false;
|
||||
|
||||
try {
|
||||
await inventoryService.deleteInventoryItem(tenantId, itemId);
|
||||
setItems(prev => prev.filter(i => i.id !== itemId));
|
||||
toast.success('Item deleted successfully');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error deleting item';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return false;
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Load stock levels
|
||||
const loadStockLevels = useCallback(async () => {
|
||||
if (!tenantId) return;
|
||||
|
||||
try {
|
||||
const levels = await inventoryService.getAllStockLevels(tenantId);
|
||||
const levelMap = levels.reduce((acc, level) => {
|
||||
acc[level.item_id] = level;
|
||||
return acc;
|
||||
}, {} as Record<string, StockLevel>);
|
||||
setStockLevels(levelMap);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading stock levels:', err);
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Adjust stock
|
||||
const adjustStock = useCallback(async (
|
||||
itemId: string,
|
||||
adjustment: StockAdjustmentRequest
|
||||
): Promise<StockMovement | null> => {
|
||||
if (!tenantId) return null;
|
||||
|
||||
try {
|
||||
const movement = await inventoryService.adjustStock(tenantId, itemId, adjustment);
|
||||
|
||||
// Update local movements
|
||||
setMovements(prev => [movement, ...prev.slice(0, 49)]); // Keep last 50
|
||||
|
||||
// Reload stock level for this item
|
||||
const updatedLevel = await inventoryService.getStockLevel(tenantId, itemId);
|
||||
setStockLevels(prev => ({ ...prev, [itemId]: updatedLevel }));
|
||||
|
||||
toast.success('Stock adjusted successfully');
|
||||
return movement;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error adjusting stock';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Load movements
|
||||
const loadMovements = useCallback(async (params?: any) => {
|
||||
if (!tenantId) return;
|
||||
|
||||
try {
|
||||
const response = await inventoryService.getStockMovements(tenantId, params);
|
||||
setMovements(response.items);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading movements:', err);
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Load alerts
|
||||
const loadAlerts = useCallback(async () => {
|
||||
if (!tenantId) return;
|
||||
|
||||
try {
|
||||
const alertsData = await inventoryService.getStockAlerts(tenantId);
|
||||
setAlerts(alertsData);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading alerts:', err);
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Acknowledge alert
|
||||
const acknowledgeAlert = useCallback(async (alertId: string): Promise<boolean> => {
|
||||
if (!tenantId) return false;
|
||||
|
||||
try {
|
||||
await inventoryService.acknowledgeAlert(tenantId, alertId);
|
||||
setAlerts(prev => prev.map(a =>
|
||||
a.id === alertId ? { ...a, is_acknowledged: true, acknowledged_at: new Date().toISOString() } : a
|
||||
));
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
toast.error('Error acknowledging alert');
|
||||
return false;
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Load dashboard
|
||||
const loadDashboard = useCallback(async () => {
|
||||
if (!tenantId) return;
|
||||
|
||||
try {
|
||||
const data = await inventoryService.getDashboardData(tenantId);
|
||||
setDashboardData(data);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading dashboard:', err);
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Search items
|
||||
const searchItems = useCallback(async (query: string): Promise<InventoryItem[]> => {
|
||||
if (!tenantId || !query.trim()) return [];
|
||||
|
||||
try {
|
||||
return await inventoryService.searchItems(tenantId, query);
|
||||
} catch (err: any) {
|
||||
console.error('Error searching items:', err);
|
||||
return [];
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Refresh all data
|
||||
const refresh = useCallback(async () => {
|
||||
await Promise.all([
|
||||
loadItems(),
|
||||
loadStockLevels(),
|
||||
loadAlerts(),
|
||||
loadDashboard()
|
||||
]);
|
||||
}, [loadItems, loadStockLevels, loadAlerts, loadDashboard]);
|
||||
|
||||
// Auto-load on mount
|
||||
useEffect(() => {
|
||||
if (autoLoad && tenantId) {
|
||||
refresh();
|
||||
}
|
||||
}, [autoLoad, tenantId, refresh]);
|
||||
|
||||
return {
|
||||
// State
|
||||
items,
|
||||
stockLevels,
|
||||
movements,
|
||||
alerts,
|
||||
dashboardData,
|
||||
isLoading,
|
||||
error,
|
||||
pagination,
|
||||
|
||||
// Actions
|
||||
loadItems,
|
||||
loadItem,
|
||||
createItem,
|
||||
updateItem,
|
||||
deleteItem,
|
||||
|
||||
// Stock operations
|
||||
loadStockLevels,
|
||||
adjustStock,
|
||||
loadMovements,
|
||||
|
||||
// Alerts
|
||||
loadAlerts,
|
||||
acknowledgeAlert,
|
||||
|
||||
// Dashboard
|
||||
loadDashboard,
|
||||
|
||||
// Utility
|
||||
searchItems,
|
||||
refresh,
|
||||
clearError
|
||||
};
|
||||
};
|
||||
|
||||
// ========== DASHBOARD HOOK ==========
|
||||
|
||||
export const useInventoryDashboard = (): UseInventoryDashboardReturn => {
|
||||
const tenantId = useTenantId();
|
||||
const [dashboardData, setDashboardData] = useState<InventoryDashboardData | null>(null);
|
||||
const [alerts, setAlerts] = useState<StockAlert[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!tenantId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const [dashboard, alertsData] = await Promise.all([
|
||||
inventoryService.getDashboardData(tenantId),
|
||||
inventoryService.getStockAlerts(tenantId)
|
||||
]);
|
||||
|
||||
setDashboardData(dashboard);
|
||||
setAlerts(alertsData);
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading dashboard';
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tenantId) {
|
||||
refresh();
|
||||
}
|
||||
}, [tenantId, refresh]);
|
||||
|
||||
return {
|
||||
dashboardData,
|
||||
alerts,
|
||||
isLoading,
|
||||
error,
|
||||
refresh
|
||||
};
|
||||
};
|
||||
|
||||
// ========== SINGLE ITEM HOOK ==========
|
||||
|
||||
export const useInventoryItem = (itemId: string): UseInventoryItemReturn => {
|
||||
const tenantId = useTenantId();
|
||||
const [item, setItem] = useState<InventoryItem | null>(null);
|
||||
const [stockLevel, setStockLevel] = useState<StockLevel | null>(null);
|
||||
const [recentMovements, setRecentMovements] = useState<StockMovement[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!tenantId || !itemId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const [itemData, stockData, movementsData] = await Promise.all([
|
||||
inventoryService.getInventoryItem(tenantId, itemId),
|
||||
inventoryService.getStockLevel(tenantId, itemId),
|
||||
inventoryService.getStockMovements(tenantId, { item_id: itemId, limit: 10 })
|
||||
]);
|
||||
|
||||
setItem(itemData);
|
||||
setStockLevel(stockData);
|
||||
setRecentMovements(movementsData.items);
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading item';
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [tenantId, itemId]);
|
||||
|
||||
const updateItem = useCallback(async (data: UpdateInventoryItemRequest): Promise<boolean> => {
|
||||
if (!tenantId || !itemId) return false;
|
||||
|
||||
try {
|
||||
const updatedItem = await inventoryService.updateInventoryItem(tenantId, itemId, data);
|
||||
setItem(updatedItem);
|
||||
toast.success('Item updated successfully');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error updating item';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return false;
|
||||
}
|
||||
}, [tenantId, itemId]);
|
||||
|
||||
const adjustStock = useCallback(async (adjustment: StockAdjustmentRequest): Promise<boolean> => {
|
||||
if (!tenantId || !itemId) return false;
|
||||
|
||||
try {
|
||||
const movement = await inventoryService.adjustStock(tenantId, itemId, adjustment);
|
||||
|
||||
// Refresh data
|
||||
const [updatedStock, updatedMovements] = await Promise.all([
|
||||
inventoryService.getStockLevel(tenantId, itemId),
|
||||
inventoryService.getStockMovements(tenantId, { item_id: itemId, limit: 10 })
|
||||
]);
|
||||
|
||||
setStockLevel(updatedStock);
|
||||
setRecentMovements(updatedMovements.items);
|
||||
|
||||
toast.success('Stock adjusted successfully');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error adjusting stock';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return false;
|
||||
}
|
||||
}, [tenantId, itemId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tenantId && itemId) {
|
||||
refresh();
|
||||
}
|
||||
}, [tenantId, itemId, refresh]);
|
||||
|
||||
return {
|
||||
item,
|
||||
stockLevel,
|
||||
recentMovements,
|
||||
isLoading,
|
||||
error,
|
||||
updateItem,
|
||||
adjustStock,
|
||||
refresh
|
||||
};
|
||||
};
|
||||
682
frontend/src/api/hooks/useRecipes.ts
Normal file
682
frontend/src/api/hooks/useRecipes.ts
Normal file
@@ -0,0 +1,682 @@
|
||||
// frontend/src/api/hooks/useRecipes.ts
|
||||
/**
|
||||
* React hooks for recipe and production management
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import {
|
||||
RecipesService,
|
||||
Recipe,
|
||||
RecipeIngredient,
|
||||
CreateRecipeRequest,
|
||||
UpdateRecipeRequest,
|
||||
RecipeSearchParams,
|
||||
RecipeFeasibility,
|
||||
RecipeStatistics,
|
||||
ProductionBatch,
|
||||
CreateProductionBatchRequest,
|
||||
UpdateProductionBatchRequest,
|
||||
ProductionBatchSearchParams,
|
||||
ProductionStatistics
|
||||
} from '../services/recipes.service';
|
||||
import { useTenant } from './useTenant';
|
||||
import { useAuth } from './useAuth';
|
||||
|
||||
const recipesService = new RecipesService();
|
||||
|
||||
// Recipe Management Hook
|
||||
export interface UseRecipesReturn {
|
||||
// Data
|
||||
recipes: Recipe[];
|
||||
selectedRecipe: Recipe | null;
|
||||
categories: string[];
|
||||
statistics: RecipeStatistics | null;
|
||||
|
||||
// State
|
||||
isLoading: boolean;
|
||||
isCreating: boolean;
|
||||
isUpdating: boolean;
|
||||
isDeleting: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Pagination
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
|
||||
// Actions
|
||||
loadRecipes: (params?: RecipeSearchParams) => Promise<void>;
|
||||
loadRecipe: (recipeId: string) => Promise<void>;
|
||||
createRecipe: (data: CreateRecipeRequest) => Promise<Recipe | null>;
|
||||
updateRecipe: (recipeId: string, data: UpdateRecipeRequest) => Promise<Recipe | null>;
|
||||
deleteRecipe: (recipeId: string) => Promise<boolean>;
|
||||
duplicateRecipe: (recipeId: string, newName: string) => Promise<Recipe | null>;
|
||||
activateRecipe: (recipeId: string) => Promise<Recipe | null>;
|
||||
checkFeasibility: (recipeId: string, batchMultiplier?: number) => Promise<RecipeFeasibility | null>;
|
||||
loadStatistics: () => Promise<void>;
|
||||
loadCategories: () => Promise<void>;
|
||||
clearError: () => void;
|
||||
refresh: () => Promise<void>;
|
||||
setPage: (page: number) => void;
|
||||
}
|
||||
|
||||
export const useRecipes = (autoLoad: boolean = true): UseRecipesReturn => {
|
||||
const { currentTenant } = useTenant();
|
||||
const { user } = useAuth();
|
||||
|
||||
// State
|
||||
const [recipes, setRecipes] = useState<Recipe[]>([]);
|
||||
const [selectedRecipe, setSelectedRecipe] = useState<Recipe | null>(null);
|
||||
const [categories, setCategories] = useState<string[]>([]);
|
||||
const [statistics, setStatistics] = useState<RecipeStatistics | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentParams, setCurrentParams] = useState<RecipeSearchParams>({});
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
});
|
||||
|
||||
// Load recipes
|
||||
const loadRecipes = useCallback(async (params: RecipeSearchParams = {}) => {
|
||||
if (!currentTenant?.id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const searchParams = {
|
||||
...params,
|
||||
limit: pagination.limit,
|
||||
offset: (pagination.page - 1) * pagination.limit
|
||||
};
|
||||
|
||||
const recipesData = await recipesService.getRecipes(currentTenant.id, searchParams);
|
||||
setRecipes(recipesData);
|
||||
setCurrentParams(params);
|
||||
|
||||
// Calculate pagination (assuming we get total count somehow)
|
||||
const total = recipesData.length; // This would need to be from a proper paginated response
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
total,
|
||||
totalPages: Math.ceil(total / prev.limit)
|
||||
}));
|
||||
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading recipes';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentTenant?.id, pagination.page, pagination.limit]);
|
||||
|
||||
// Load single recipe
|
||||
const loadRecipe = useCallback(async (recipeId: string) => {
|
||||
if (!currentTenant?.id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const recipe = await recipesService.getRecipe(currentTenant.id, recipeId);
|
||||
setSelectedRecipe(recipe);
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading recipe';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentTenant?.id]);
|
||||
|
||||
// Create recipe
|
||||
const createRecipe = useCallback(async (data: CreateRecipeRequest): Promise<Recipe | null> => {
|
||||
if (!currentTenant?.id || !user?.id) return null;
|
||||
|
||||
setIsCreating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const newRecipe = await recipesService.createRecipe(currentTenant.id, user.id, data);
|
||||
|
||||
// Add to local state
|
||||
setRecipes(prev => [newRecipe, ...prev]);
|
||||
|
||||
toast.success('Recipe created successfully');
|
||||
return newRecipe;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error creating recipe';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}, [currentTenant?.id, user?.id]);
|
||||
|
||||
// Update recipe
|
||||
const updateRecipe = useCallback(async (recipeId: string, data: UpdateRecipeRequest): Promise<Recipe | null> => {
|
||||
if (!currentTenant?.id || !user?.id) return null;
|
||||
|
||||
setIsUpdating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const updatedRecipe = await recipesService.updateRecipe(currentTenant.id, user.id, recipeId, data);
|
||||
|
||||
// Update local state
|
||||
setRecipes(prev => prev.map(recipe =>
|
||||
recipe.id === recipeId ? updatedRecipe : recipe
|
||||
));
|
||||
|
||||
if (selectedRecipe?.id === recipeId) {
|
||||
setSelectedRecipe(updatedRecipe);
|
||||
}
|
||||
|
||||
toast.success('Recipe updated successfully');
|
||||
return updatedRecipe;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error updating recipe';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [currentTenant?.id, user?.id, selectedRecipe?.id]);
|
||||
|
||||
// Delete recipe
|
||||
const deleteRecipe = useCallback(async (recipeId: string): Promise<boolean> => {
|
||||
if (!currentTenant?.id) return false;
|
||||
|
||||
setIsDeleting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await recipesService.deleteRecipe(currentTenant.id, recipeId);
|
||||
|
||||
// Remove from local state
|
||||
setRecipes(prev => prev.filter(recipe => recipe.id !== recipeId));
|
||||
|
||||
if (selectedRecipe?.id === recipeId) {
|
||||
setSelectedRecipe(null);
|
||||
}
|
||||
|
||||
toast.success('Recipe deleted successfully');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error deleting recipe';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return false;
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
}, [currentTenant?.id, selectedRecipe?.id]);
|
||||
|
||||
// Duplicate recipe
|
||||
const duplicateRecipe = useCallback(async (recipeId: string, newName: string): Promise<Recipe | null> => {
|
||||
if (!currentTenant?.id || !user?.id) return null;
|
||||
|
||||
setIsCreating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const duplicatedRecipe = await recipesService.duplicateRecipe(currentTenant.id, user.id, recipeId, newName);
|
||||
|
||||
// Add to local state
|
||||
setRecipes(prev => [duplicatedRecipe, ...prev]);
|
||||
|
||||
toast.success('Recipe duplicated successfully');
|
||||
return duplicatedRecipe;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error duplicating recipe';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}, [currentTenant?.id, user?.id]);
|
||||
|
||||
// Activate recipe
|
||||
const activateRecipe = useCallback(async (recipeId: string): Promise<Recipe | null> => {
|
||||
if (!currentTenant?.id || !user?.id) return null;
|
||||
|
||||
setIsUpdating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const activatedRecipe = await recipesService.activateRecipe(currentTenant.id, user.id, recipeId);
|
||||
|
||||
// Update local state
|
||||
setRecipes(prev => prev.map(recipe =>
|
||||
recipe.id === recipeId ? activatedRecipe : recipe
|
||||
));
|
||||
|
||||
if (selectedRecipe?.id === recipeId) {
|
||||
setSelectedRecipe(activatedRecipe);
|
||||
}
|
||||
|
||||
toast.success('Recipe activated successfully');
|
||||
return activatedRecipe;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error activating recipe';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [currentTenant?.id, user?.id, selectedRecipe?.id]);
|
||||
|
||||
// Check feasibility
|
||||
const checkFeasibility = useCallback(async (recipeId: string, batchMultiplier: number = 1.0): Promise<RecipeFeasibility | null> => {
|
||||
if (!currentTenant?.id) return null;
|
||||
|
||||
try {
|
||||
const feasibility = await recipesService.checkRecipeFeasibility(currentTenant.id, recipeId, batchMultiplier);
|
||||
return feasibility;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error checking recipe feasibility';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
}
|
||||
}, [currentTenant?.id]);
|
||||
|
||||
// Load statistics
|
||||
const loadStatistics = useCallback(async () => {
|
||||
if (!currentTenant?.id) return;
|
||||
|
||||
try {
|
||||
const stats = await recipesService.getRecipeStatistics(currentTenant.id);
|
||||
setStatistics(stats);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading recipe statistics:', err);
|
||||
}
|
||||
}, [currentTenant?.id]);
|
||||
|
||||
// Load categories
|
||||
const loadCategories = useCallback(async () => {
|
||||
if (!currentTenant?.id) return;
|
||||
|
||||
try {
|
||||
const cats = await recipesService.getRecipeCategories(currentTenant.id);
|
||||
setCategories(cats);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading recipe categories:', err);
|
||||
}
|
||||
}, [currentTenant?.id]);
|
||||
|
||||
// Clear error
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
// Refresh
|
||||
const refresh = useCallback(async () => {
|
||||
await Promise.all([
|
||||
loadRecipes(currentParams),
|
||||
loadStatistics(),
|
||||
loadCategories()
|
||||
]);
|
||||
}, [loadRecipes, currentParams, loadStatistics, loadCategories]);
|
||||
|
||||
// Set page
|
||||
const setPage = useCallback((page: number) => {
|
||||
setPagination(prev => ({ ...prev, page }));
|
||||
}, []);
|
||||
|
||||
// Auto-load on mount and dependencies change
|
||||
useEffect(() => {
|
||||
if (autoLoad && currentTenant?.id) {
|
||||
refresh();
|
||||
}
|
||||
}, [autoLoad, currentTenant?.id, pagination.page]);
|
||||
|
||||
return {
|
||||
// Data
|
||||
recipes,
|
||||
selectedRecipe,
|
||||
categories,
|
||||
statistics,
|
||||
|
||||
// State
|
||||
isLoading,
|
||||
isCreating,
|
||||
isUpdating,
|
||||
isDeleting,
|
||||
error,
|
||||
pagination,
|
||||
|
||||
// Actions
|
||||
loadRecipes,
|
||||
loadRecipe,
|
||||
createRecipe,
|
||||
updateRecipe,
|
||||
deleteRecipe,
|
||||
duplicateRecipe,
|
||||
activateRecipe,
|
||||
checkFeasibility,
|
||||
loadStatistics,
|
||||
loadCategories,
|
||||
clearError,
|
||||
refresh,
|
||||
setPage
|
||||
};
|
||||
};
|
||||
|
||||
// Production Management Hook
|
||||
export interface UseProductionReturn {
|
||||
// Data
|
||||
batches: ProductionBatch[];
|
||||
selectedBatch: ProductionBatch | null;
|
||||
activeBatches: ProductionBatch[];
|
||||
statistics: ProductionStatistics | null;
|
||||
|
||||
// State
|
||||
isLoading: boolean;
|
||||
isCreating: boolean;
|
||||
isUpdating: boolean;
|
||||
isDeleting: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
loadBatches: (params?: ProductionBatchSearchParams) => Promise<void>;
|
||||
loadBatch: (batchId: string) => Promise<void>;
|
||||
loadActiveBatches: () => Promise<void>;
|
||||
createBatch: (data: CreateProductionBatchRequest) => Promise<ProductionBatch | null>;
|
||||
updateBatch: (batchId: string, data: UpdateProductionBatchRequest) => Promise<ProductionBatch | null>;
|
||||
deleteBatch: (batchId: string) => Promise<boolean>;
|
||||
startBatch: (batchId: string, data: any) => Promise<ProductionBatch | null>;
|
||||
completeBatch: (batchId: string, data: any) => Promise<ProductionBatch | null>;
|
||||
loadStatistics: (startDate?: string, endDate?: string) => Promise<void>;
|
||||
clearError: () => void;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useProduction = (autoLoad: boolean = true): UseProductionReturn => {
|
||||
const { currentTenant } = useTenant();
|
||||
const { user } = useAuth();
|
||||
|
||||
// State
|
||||
const [batches, setBatches] = useState<ProductionBatch[]>([]);
|
||||
const [selectedBatch, setSelectedBatch] = useState<ProductionBatch | null>(null);
|
||||
const [activeBatches, setActiveBatches] = useState<ProductionBatch[]>([]);
|
||||
const [statistics, setStatistics] = useState<ProductionStatistics | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Load batches
|
||||
const loadBatches = useCallback(async (params: ProductionBatchSearchParams = {}) => {
|
||||
if (!currentTenant?.id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const batchesData = await recipesService.getProductionBatches(currentTenant.id, params);
|
||||
setBatches(batchesData);
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading production batches';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentTenant?.id]);
|
||||
|
||||
// Load single batch
|
||||
const loadBatch = useCallback(async (batchId: string) => {
|
||||
if (!currentTenant?.id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const batch = await recipesService.getProductionBatch(currentTenant.id, batchId);
|
||||
setSelectedBatch(batch);
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading production batch';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentTenant?.id]);
|
||||
|
||||
// Load active batches
|
||||
const loadActiveBatches = useCallback(async () => {
|
||||
if (!currentTenant?.id) return;
|
||||
|
||||
try {
|
||||
const activeBatchesData = await recipesService.getActiveProductionBatches(currentTenant.id);
|
||||
setActiveBatches(activeBatchesData);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading active batches:', err);
|
||||
}
|
||||
}, [currentTenant?.id]);
|
||||
|
||||
// Create batch
|
||||
const createBatch = useCallback(async (data: CreateProductionBatchRequest): Promise<ProductionBatch | null> => {
|
||||
if (!currentTenant?.id || !user?.id) return null;
|
||||
|
||||
setIsCreating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const newBatch = await recipesService.createProductionBatch(currentTenant.id, user.id, data);
|
||||
|
||||
// Add to local state
|
||||
setBatches(prev => [newBatch, ...prev]);
|
||||
|
||||
toast.success('Production batch created successfully');
|
||||
return newBatch;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error creating production batch';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}, [currentTenant?.id, user?.id]);
|
||||
|
||||
// Update batch
|
||||
const updateBatch = useCallback(async (batchId: string, data: UpdateProductionBatchRequest): Promise<ProductionBatch | null> => {
|
||||
if (!currentTenant?.id || !user?.id) return null;
|
||||
|
||||
setIsUpdating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const updatedBatch = await recipesService.updateProductionBatch(currentTenant.id, user.id, batchId, data);
|
||||
|
||||
// Update local state
|
||||
setBatches(prev => prev.map(batch =>
|
||||
batch.id === batchId ? updatedBatch : batch
|
||||
));
|
||||
|
||||
if (selectedBatch?.id === batchId) {
|
||||
setSelectedBatch(updatedBatch);
|
||||
}
|
||||
|
||||
toast.success('Production batch updated successfully');
|
||||
return updatedBatch;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error updating production batch';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [currentTenant?.id, user?.id, selectedBatch?.id]);
|
||||
|
||||
// Delete batch
|
||||
const deleteBatch = useCallback(async (batchId: string): Promise<boolean> => {
|
||||
if (!currentTenant?.id) return false;
|
||||
|
||||
setIsDeleting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await recipesService.deleteProductionBatch(currentTenant.id, batchId);
|
||||
|
||||
// Remove from local state
|
||||
setBatches(prev => prev.filter(batch => batch.id !== batchId));
|
||||
|
||||
if (selectedBatch?.id === batchId) {
|
||||
setSelectedBatch(null);
|
||||
}
|
||||
|
||||
toast.success('Production batch deleted successfully');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error deleting production batch';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return false;
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
}, [currentTenant?.id, selectedBatch?.id]);
|
||||
|
||||
// Start batch
|
||||
const startBatch = useCallback(async (batchId: string, data: any): Promise<ProductionBatch | null> => {
|
||||
if (!currentTenant?.id || !user?.id) return null;
|
||||
|
||||
setIsUpdating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const startedBatch = await recipesService.startProductionBatch(currentTenant.id, user.id, batchId, data);
|
||||
|
||||
// Update local state
|
||||
setBatches(prev => prev.map(batch =>
|
||||
batch.id === batchId ? startedBatch : batch
|
||||
));
|
||||
|
||||
if (selectedBatch?.id === batchId) {
|
||||
setSelectedBatch(startedBatch);
|
||||
}
|
||||
|
||||
toast.success('Production batch started successfully');
|
||||
return startedBatch;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error starting production batch';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [currentTenant?.id, user?.id, selectedBatch?.id]);
|
||||
|
||||
// Complete batch
|
||||
const completeBatch = useCallback(async (batchId: string, data: any): Promise<ProductionBatch | null> => {
|
||||
if (!currentTenant?.id || !user?.id) return null;
|
||||
|
||||
setIsUpdating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const completedBatch = await recipesService.completeProductionBatch(currentTenant.id, user.id, batchId, data);
|
||||
|
||||
// Update local state
|
||||
setBatches(prev => prev.map(batch =>
|
||||
batch.id === batchId ? completedBatch : batch
|
||||
));
|
||||
|
||||
if (selectedBatch?.id === batchId) {
|
||||
setSelectedBatch(completedBatch);
|
||||
}
|
||||
|
||||
toast.success('Production batch completed successfully');
|
||||
return completedBatch;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error completing production batch';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [currentTenant?.id, user?.id, selectedBatch?.id]);
|
||||
|
||||
// Load statistics
|
||||
const loadStatistics = useCallback(async (startDate?: string, endDate?: string) => {
|
||||
if (!currentTenant?.id) return;
|
||||
|
||||
try {
|
||||
const stats = await recipesService.getProductionStatistics(currentTenant.id, startDate, endDate);
|
||||
setStatistics(stats);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading production statistics:', err);
|
||||
}
|
||||
}, [currentTenant?.id]);
|
||||
|
||||
// Clear error
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
// Refresh
|
||||
const refresh = useCallback(async () => {
|
||||
await Promise.all([
|
||||
loadBatches(),
|
||||
loadActiveBatches(),
|
||||
loadStatistics()
|
||||
]);
|
||||
}, [loadBatches, loadActiveBatches, loadStatistics]);
|
||||
|
||||
// Auto-load on mount
|
||||
useEffect(() => {
|
||||
if (autoLoad && currentTenant?.id) {
|
||||
refresh();
|
||||
}
|
||||
}, [autoLoad, currentTenant?.id]);
|
||||
|
||||
return {
|
||||
// Data
|
||||
batches,
|
||||
selectedBatch,
|
||||
activeBatches,
|
||||
statistics,
|
||||
|
||||
// State
|
||||
isLoading,
|
||||
isCreating,
|
||||
isUpdating,
|
||||
isDeleting,
|
||||
error,
|
||||
|
||||
// Actions
|
||||
loadBatches,
|
||||
loadBatch,
|
||||
loadActiveBatches,
|
||||
createBatch,
|
||||
updateBatch,
|
||||
deleteBatch,
|
||||
startBatch,
|
||||
completeBatch,
|
||||
loadStatistics,
|
||||
clearError,
|
||||
refresh
|
||||
};
|
||||
};
|
||||
890
frontend/src/api/hooks/useSuppliers.ts
Normal file
890
frontend/src/api/hooks/useSuppliers.ts
Normal file
@@ -0,0 +1,890 @@
|
||||
// frontend/src/api/hooks/useSuppliers.ts
|
||||
/**
|
||||
* React hooks for suppliers, purchase orders, and deliveries management
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
SuppliersService,
|
||||
Supplier,
|
||||
SupplierSummary,
|
||||
CreateSupplierRequest,
|
||||
UpdateSupplierRequest,
|
||||
SupplierSearchParams,
|
||||
SupplierStatistics,
|
||||
PurchaseOrder,
|
||||
CreatePurchaseOrderRequest,
|
||||
PurchaseOrderSearchParams,
|
||||
PurchaseOrderStatistics,
|
||||
Delivery,
|
||||
DeliverySearchParams,
|
||||
DeliveryPerformanceStats
|
||||
} from '../services/suppliers.service';
|
||||
import { useAuth } from './useAuth';
|
||||
|
||||
const suppliersService = new SuppliersService();
|
||||
|
||||
// ============================================================================
|
||||
// SUPPLIERS HOOK
|
||||
// ============================================================================
|
||||
|
||||
export interface UseSuppliers {
|
||||
// Data
|
||||
suppliers: SupplierSummary[];
|
||||
supplier: Supplier | null;
|
||||
statistics: SupplierStatistics | null;
|
||||
activeSuppliers: SupplierSummary[];
|
||||
topSuppliers: SupplierSummary[];
|
||||
suppliersNeedingReview: SupplierSummary[];
|
||||
|
||||
// States
|
||||
isLoading: boolean;
|
||||
isCreating: boolean;
|
||||
isUpdating: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Pagination
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
|
||||
// Actions
|
||||
loadSuppliers: (params?: SupplierSearchParams) => Promise<void>;
|
||||
loadSupplier: (supplierId: string) => Promise<void>;
|
||||
loadStatistics: () => Promise<void>;
|
||||
loadActiveSuppliers: () => Promise<void>;
|
||||
loadTopSuppliers: (limit?: number) => Promise<void>;
|
||||
loadSuppliersNeedingReview: (days?: number) => Promise<void>;
|
||||
createSupplier: (data: CreateSupplierRequest) => Promise<Supplier | null>;
|
||||
updateSupplier: (supplierId: string, data: UpdateSupplierRequest) => Promise<Supplier | null>;
|
||||
deleteSupplier: (supplierId: string) => Promise<boolean>;
|
||||
approveSupplier: (supplierId: string, action: 'approve' | 'reject', notes?: string) => Promise<Supplier | null>;
|
||||
clearError: () => void;
|
||||
refresh: () => Promise<void>;
|
||||
setPage: (page: number) => void;
|
||||
}
|
||||
|
||||
export function useSuppliers(): UseSuppliers {
|
||||
const { user } = useAuth();
|
||||
|
||||
// State
|
||||
const [suppliers, setSuppliers] = useState<SupplierSummary[]>([]);
|
||||
const [supplier, setSupplier] = useState<Supplier | null>(null);
|
||||
const [statistics, setStatistics] = useState<SupplierStatistics | null>(null);
|
||||
const [activeSuppliers, setActiveSuppliers] = useState<SupplierSummary[]>([]);
|
||||
const [topSuppliers, setTopSuppliers] = useState<SupplierSummary[]>([]);
|
||||
const [suppliersNeedingReview, setSuppliersNeedingReview] = useState<SupplierSummary[]>([]);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [currentParams, setCurrentParams] = useState<SupplierSearchParams>({});
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
});
|
||||
|
||||
// Load suppliers
|
||||
const loadSuppliers = useCallback(async (params: SupplierSearchParams = {}) => {
|
||||
if (!user?.tenant_id) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const searchParams = {
|
||||
...params,
|
||||
limit: pagination.limit,
|
||||
offset: ((params.offset !== undefined ? Math.floor(params.offset / pagination.limit) : pagination.page) - 1) * pagination.limit
|
||||
};
|
||||
|
||||
setCurrentParams(params);
|
||||
|
||||
const data = await suppliersService.getSuppliers(user.tenant_id, searchParams);
|
||||
setSuppliers(data);
|
||||
|
||||
// Update pagination (Note: API doesn't return total count, so we estimate)
|
||||
const hasMore = data.length === pagination.limit;
|
||||
const currentPage = Math.floor((searchParams.offset || 0) / pagination.limit) + 1;
|
||||
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
page: currentPage,
|
||||
total: hasMore ? (currentPage * pagination.limit) + 1 : (currentPage - 1) * pagination.limit + data.length,
|
||||
totalPages: hasMore ? currentPage + 1 : currentPage
|
||||
}));
|
||||
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || err.message || 'Failed to load suppliers');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [user?.tenant_id, pagination.limit]);
|
||||
|
||||
// Load single supplier
|
||||
const loadSupplier = useCallback(async (supplierId: string) => {
|
||||
if (!user?.tenant_id) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = await suppliersService.getSupplier(user.tenant_id, supplierId);
|
||||
setSupplier(data);
|
||||
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || err.message || 'Failed to load supplier');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [user?.tenant_id]);
|
||||
|
||||
// Load statistics
|
||||
const loadStatistics = useCallback(async () => {
|
||||
if (!user?.tenant_id) return;
|
||||
|
||||
try {
|
||||
const data = await suppliersService.getSupplierStatistics(user.tenant_id);
|
||||
setStatistics(data);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load supplier statistics:', err);
|
||||
}
|
||||
}, [user?.tenant_id]);
|
||||
|
||||
// Load active suppliers
|
||||
const loadActiveSuppliers = useCallback(async () => {
|
||||
if (!user?.tenant_id) return;
|
||||
|
||||
try {
|
||||
const data = await suppliersService.getActiveSuppliers(user.tenant_id);
|
||||
setActiveSuppliers(data);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load active suppliers:', err);
|
||||
}
|
||||
}, [user?.tenant_id]);
|
||||
|
||||
// Load top suppliers
|
||||
const loadTopSuppliers = useCallback(async (limit: number = 10) => {
|
||||
if (!user?.tenant_id) return;
|
||||
|
||||
try {
|
||||
const data = await suppliersService.getTopSuppliers(user.tenant_id, limit);
|
||||
setTopSuppliers(data);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load top suppliers:', err);
|
||||
}
|
||||
}, [user?.tenant_id]);
|
||||
|
||||
// Load suppliers needing review
|
||||
const loadSuppliersNeedingReview = useCallback(async (days: number = 30) => {
|
||||
if (!user?.tenant_id) return;
|
||||
|
||||
try {
|
||||
const data = await suppliersService.getSuppliersNeedingReview(user.tenant_id, days);
|
||||
setSuppliersNeedingReview(data);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load suppliers needing review:', err);
|
||||
}
|
||||
}, [user?.tenant_id]);
|
||||
|
||||
// Create supplier
|
||||
const createSupplier = useCallback(async (data: CreateSupplierRequest): Promise<Supplier | null> => {
|
||||
if (!user?.tenant_id || !user?.user_id) return null;
|
||||
|
||||
try {
|
||||
setIsCreating(true);
|
||||
setError(null);
|
||||
|
||||
const supplier = await suppliersService.createSupplier(user.tenant_id, user.user_id, data);
|
||||
|
||||
// Refresh suppliers list
|
||||
await loadSuppliers(currentParams);
|
||||
await loadStatistics();
|
||||
|
||||
return supplier;
|
||||
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to create supplier';
|
||||
setError(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}, [user?.tenant_id, user?.user_id, loadSuppliers, loadStatistics, currentParams]);
|
||||
|
||||
// Update supplier
|
||||
const updateSupplier = useCallback(async (supplierId: string, data: UpdateSupplierRequest): Promise<Supplier | null> => {
|
||||
if (!user?.tenant_id || !user?.user_id) return null;
|
||||
|
||||
try {
|
||||
setIsUpdating(true);
|
||||
setError(null);
|
||||
|
||||
const updatedSupplier = await suppliersService.updateSupplier(user.tenant_id, user.user_id, supplierId, data);
|
||||
|
||||
// Update current supplier if it's the one being edited
|
||||
if (supplier?.id === supplierId) {
|
||||
setSupplier(updatedSupplier);
|
||||
}
|
||||
|
||||
// Refresh suppliers list
|
||||
await loadSuppliers(currentParams);
|
||||
|
||||
return updatedSupplier;
|
||||
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to update supplier';
|
||||
setError(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [user?.tenant_id, user?.user_id, supplier?.id, loadSuppliers, currentParams]);
|
||||
|
||||
// Delete supplier
|
||||
const deleteSupplier = useCallback(async (supplierId: string): Promise<boolean> => {
|
||||
if (!user?.tenant_id) return false;
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
await suppliersService.deleteSupplier(user.tenant_id, supplierId);
|
||||
|
||||
// Clear current supplier if it's the one being deleted
|
||||
if (supplier?.id === supplierId) {
|
||||
setSupplier(null);
|
||||
}
|
||||
|
||||
// Refresh suppliers list
|
||||
await loadSuppliers(currentParams);
|
||||
await loadStatistics();
|
||||
|
||||
return true;
|
||||
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to delete supplier';
|
||||
setError(errorMessage);
|
||||
return false;
|
||||
}
|
||||
}, [user?.tenant_id, supplier?.id, loadSuppliers, loadStatistics, currentParams]);
|
||||
|
||||
// Approve/reject supplier
|
||||
const approveSupplier = useCallback(async (supplierId: string, action: 'approve' | 'reject', notes?: string): Promise<Supplier | null> => {
|
||||
if (!user?.tenant_id || !user?.user_id) return null;
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
const updatedSupplier = await suppliersService.approveSupplier(user.tenant_id, user.user_id, supplierId, action, notes);
|
||||
|
||||
// Update current supplier if it's the one being approved/rejected
|
||||
if (supplier?.id === supplierId) {
|
||||
setSupplier(updatedSupplier);
|
||||
}
|
||||
|
||||
// Refresh suppliers list and statistics
|
||||
await loadSuppliers(currentParams);
|
||||
await loadStatistics();
|
||||
|
||||
return updatedSupplier;
|
||||
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || `Failed to ${action} supplier`;
|
||||
setError(errorMessage);
|
||||
return null;
|
||||
}
|
||||
}, [user?.tenant_id, user?.user_id, supplier?.id, loadSuppliers, loadStatistics, currentParams]);
|
||||
|
||||
// Clear error
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
// Refresh current data
|
||||
const refresh = useCallback(async () => {
|
||||
await loadSuppliers(currentParams);
|
||||
if (statistics) await loadStatistics();
|
||||
if (activeSuppliers.length > 0) await loadActiveSuppliers();
|
||||
if (topSuppliers.length > 0) await loadTopSuppliers();
|
||||
if (suppliersNeedingReview.length > 0) await loadSuppliersNeedingReview();
|
||||
}, [currentParams, statistics, activeSuppliers.length, topSuppliers.length, suppliersNeedingReview.length, loadSuppliers, loadStatistics, loadActiveSuppliers, loadTopSuppliers, loadSuppliersNeedingReview]);
|
||||
|
||||
// Set page
|
||||
const setPage = useCallback((page: number) => {
|
||||
setPagination(prev => ({ ...prev, page }));
|
||||
const offset = (page - 1) * pagination.limit;
|
||||
loadSuppliers({ ...currentParams, offset });
|
||||
}, [pagination.limit, currentParams, loadSuppliers]);
|
||||
|
||||
return {
|
||||
// Data
|
||||
suppliers,
|
||||
supplier,
|
||||
statistics,
|
||||
activeSuppliers,
|
||||
topSuppliers,
|
||||
suppliersNeedingReview,
|
||||
|
||||
// States
|
||||
isLoading,
|
||||
isCreating,
|
||||
isUpdating,
|
||||
error,
|
||||
|
||||
// Pagination
|
||||
pagination,
|
||||
|
||||
// Actions
|
||||
loadSuppliers,
|
||||
loadSupplier,
|
||||
loadStatistics,
|
||||
loadActiveSuppliers,
|
||||
loadTopSuppliers,
|
||||
loadSuppliersNeedingReview,
|
||||
createSupplier,
|
||||
updateSupplier,
|
||||
deleteSupplier,
|
||||
approveSupplier,
|
||||
clearError,
|
||||
refresh,
|
||||
setPage
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PURCHASE ORDERS HOOK
|
||||
// ============================================================================
|
||||
|
||||
export interface UsePurchaseOrders {
|
||||
purchaseOrders: PurchaseOrder[];
|
||||
purchaseOrder: PurchaseOrder | null;
|
||||
statistics: PurchaseOrderStatistics | null;
|
||||
ordersRequiringApproval: PurchaseOrder[];
|
||||
overdueOrders: PurchaseOrder[];
|
||||
isLoading: boolean;
|
||||
isCreating: boolean;
|
||||
error: string | null;
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
|
||||
loadPurchaseOrders: (params?: PurchaseOrderSearchParams) => Promise<void>;
|
||||
loadPurchaseOrder: (poId: string) => Promise<void>;
|
||||
loadStatistics: () => Promise<void>;
|
||||
loadOrdersRequiringApproval: () => Promise<void>;
|
||||
loadOverdueOrders: () => Promise<void>;
|
||||
createPurchaseOrder: (data: CreatePurchaseOrderRequest) => Promise<PurchaseOrder | null>;
|
||||
updateOrderStatus: (poId: string, status: string, notes?: string) => Promise<PurchaseOrder | null>;
|
||||
approveOrder: (poId: string, action: 'approve' | 'reject', notes?: string) => Promise<PurchaseOrder | null>;
|
||||
sendToSupplier: (poId: string, sendEmail?: boolean) => Promise<PurchaseOrder | null>;
|
||||
cancelOrder: (poId: string, reason: string) => Promise<PurchaseOrder | null>;
|
||||
clearError: () => void;
|
||||
refresh: () => Promise<void>;
|
||||
setPage: (page: number) => void;
|
||||
}
|
||||
|
||||
export function usePurchaseOrders(): UsePurchaseOrders {
|
||||
const { user } = useAuth();
|
||||
|
||||
// State
|
||||
const [purchaseOrders, setPurchaseOrders] = useState<PurchaseOrder[]>([]);
|
||||
const [purchaseOrder, setPurchaseOrder] = useState<PurchaseOrder | null>(null);
|
||||
const [statistics, setStatistics] = useState<PurchaseOrderStatistics | null>(null);
|
||||
const [ordersRequiringApproval, setOrdersRequiringApproval] = useState<PurchaseOrder[]>([]);
|
||||
const [overdueOrders, setOverdueOrders] = useState<PurchaseOrder[]>([]);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [currentParams, setCurrentParams] = useState<PurchaseOrderSearchParams>({});
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
});
|
||||
|
||||
// Load purchase orders
|
||||
const loadPurchaseOrders = useCallback(async (params: PurchaseOrderSearchParams = {}) => {
|
||||
if (!user?.tenant_id) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const searchParams = {
|
||||
...params,
|
||||
limit: pagination.limit,
|
||||
offset: ((params.offset !== undefined ? Math.floor(params.offset / pagination.limit) : pagination.page) - 1) * pagination.limit
|
||||
};
|
||||
|
||||
setCurrentParams(params);
|
||||
|
||||
const data = await suppliersService.getPurchaseOrders(user.tenant_id, searchParams);
|
||||
setPurchaseOrders(data);
|
||||
|
||||
// Update pagination
|
||||
const hasMore = data.length === pagination.limit;
|
||||
const currentPage = Math.floor((searchParams.offset || 0) / pagination.limit) + 1;
|
||||
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
page: currentPage,
|
||||
total: hasMore ? (currentPage * pagination.limit) + 1 : (currentPage - 1) * pagination.limit + data.length,
|
||||
totalPages: hasMore ? currentPage + 1 : currentPage
|
||||
}));
|
||||
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || err.message || 'Failed to load purchase orders');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [user?.tenant_id, pagination.limit]);
|
||||
|
||||
// Other purchase order methods...
|
||||
const loadPurchaseOrder = useCallback(async (poId: string) => {
|
||||
if (!user?.tenant_id) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = await suppliersService.getPurchaseOrder(user.tenant_id, poId);
|
||||
setPurchaseOrder(data);
|
||||
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || err.message || 'Failed to load purchase order');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [user?.tenant_id]);
|
||||
|
||||
const loadStatistics = useCallback(async () => {
|
||||
if (!user?.tenant_id) return;
|
||||
|
||||
try {
|
||||
const data = await suppliersService.getPurchaseOrderStatistics(user.tenant_id);
|
||||
setStatistics(data);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load purchase order statistics:', err);
|
||||
}
|
||||
}, [user?.tenant_id]);
|
||||
|
||||
const loadOrdersRequiringApproval = useCallback(async () => {
|
||||
if (!user?.tenant_id) return;
|
||||
|
||||
try {
|
||||
const data = await suppliersService.getOrdersRequiringApproval(user.tenant_id);
|
||||
setOrdersRequiringApproval(data);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load orders requiring approval:', err);
|
||||
}
|
||||
}, [user?.tenant_id]);
|
||||
|
||||
const loadOverdueOrders = useCallback(async () => {
|
||||
if (!user?.tenant_id) return;
|
||||
|
||||
try {
|
||||
const data = await suppliersService.getOverdueOrders(user.tenant_id);
|
||||
setOverdueOrders(data);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load overdue orders:', err);
|
||||
}
|
||||
}, [user?.tenant_id]);
|
||||
|
||||
const createPurchaseOrder = useCallback(async (data: CreatePurchaseOrderRequest): Promise<PurchaseOrder | null> => {
|
||||
if (!user?.tenant_id || !user?.user_id) return null;
|
||||
|
||||
try {
|
||||
setIsCreating(true);
|
||||
setError(null);
|
||||
|
||||
const order = await suppliersService.createPurchaseOrder(user.tenant_id, user.user_id, data);
|
||||
|
||||
// Refresh orders list
|
||||
await loadPurchaseOrders(currentParams);
|
||||
await loadStatistics();
|
||||
|
||||
return order;
|
||||
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to create purchase order';
|
||||
setError(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}, [user?.tenant_id, user?.user_id, loadPurchaseOrders, loadStatistics, currentParams]);
|
||||
|
||||
const updateOrderStatus = useCallback(async (poId: string, status: string, notes?: string): Promise<PurchaseOrder | null> => {
|
||||
if (!user?.tenant_id || !user?.user_id) return null;
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
const updatedOrder = await suppliersService.updatePurchaseOrderStatus(user.tenant_id, user.user_id, poId, status, notes);
|
||||
|
||||
if (purchaseOrder?.id === poId) {
|
||||
setPurchaseOrder(updatedOrder);
|
||||
}
|
||||
|
||||
await loadPurchaseOrders(currentParams);
|
||||
|
||||
return updatedOrder;
|
||||
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to update order status';
|
||||
setError(errorMessage);
|
||||
return null;
|
||||
}
|
||||
}, [user?.tenant_id, user?.user_id, purchaseOrder?.id, loadPurchaseOrders, currentParams]);
|
||||
|
||||
const approveOrder = useCallback(async (poId: string, action: 'approve' | 'reject', notes?: string): Promise<PurchaseOrder | null> => {
|
||||
if (!user?.tenant_id || !user?.user_id) return null;
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
const updatedOrder = await suppliersService.approvePurchaseOrder(user.tenant_id, user.user_id, poId, action, notes);
|
||||
|
||||
if (purchaseOrder?.id === poId) {
|
||||
setPurchaseOrder(updatedOrder);
|
||||
}
|
||||
|
||||
await loadPurchaseOrders(currentParams);
|
||||
await loadOrdersRequiringApproval();
|
||||
|
||||
return updatedOrder;
|
||||
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || `Failed to ${action} order`;
|
||||
setError(errorMessage);
|
||||
return null;
|
||||
}
|
||||
}, [user?.tenant_id, user?.user_id, purchaseOrder?.id, loadPurchaseOrders, loadOrdersRequiringApproval, currentParams]);
|
||||
|
||||
const sendToSupplier = useCallback(async (poId: string, sendEmail: boolean = true): Promise<PurchaseOrder | null> => {
|
||||
if (!user?.tenant_id || !user?.user_id) return null;
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
const updatedOrder = await suppliersService.sendToSupplier(user.tenant_id, user.user_id, poId, sendEmail);
|
||||
|
||||
if (purchaseOrder?.id === poId) {
|
||||
setPurchaseOrder(updatedOrder);
|
||||
}
|
||||
|
||||
await loadPurchaseOrders(currentParams);
|
||||
|
||||
return updatedOrder;
|
||||
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to send order to supplier';
|
||||
setError(errorMessage);
|
||||
return null;
|
||||
}
|
||||
}, [user?.tenant_id, user?.user_id, purchaseOrder?.id, loadPurchaseOrders, currentParams]);
|
||||
|
||||
const cancelOrder = useCallback(async (poId: string, reason: string): Promise<PurchaseOrder | null> => {
|
||||
if (!user?.tenant_id || !user?.user_id) return null;
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
const updatedOrder = await suppliersService.cancelPurchaseOrder(user.tenant_id, user.user_id, poId, reason);
|
||||
|
||||
if (purchaseOrder?.id === poId) {
|
||||
setPurchaseOrder(updatedOrder);
|
||||
}
|
||||
|
||||
await loadPurchaseOrders(currentParams);
|
||||
|
||||
return updatedOrder;
|
||||
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to cancel order';
|
||||
setError(errorMessage);
|
||||
return null;
|
||||
}
|
||||
}, [user?.tenant_id, user?.user_id, purchaseOrder?.id, loadPurchaseOrders, currentParams]);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await loadPurchaseOrders(currentParams);
|
||||
if (statistics) await loadStatistics();
|
||||
if (ordersRequiringApproval.length > 0) await loadOrdersRequiringApproval();
|
||||
if (overdueOrders.length > 0) await loadOverdueOrders();
|
||||
}, [currentParams, statistics, ordersRequiringApproval.length, overdueOrders.length, loadPurchaseOrders, loadStatistics, loadOrdersRequiringApproval, loadOverdueOrders]);
|
||||
|
||||
const setPage = useCallback((page: number) => {
|
||||
setPagination(prev => ({ ...prev, page }));
|
||||
const offset = (page - 1) * pagination.limit;
|
||||
loadPurchaseOrders({ ...currentParams, offset });
|
||||
}, [pagination.limit, currentParams, loadPurchaseOrders]);
|
||||
|
||||
return {
|
||||
purchaseOrders,
|
||||
purchaseOrder,
|
||||
statistics,
|
||||
ordersRequiringApproval,
|
||||
overdueOrders,
|
||||
isLoading,
|
||||
isCreating,
|
||||
error,
|
||||
pagination,
|
||||
|
||||
loadPurchaseOrders,
|
||||
loadPurchaseOrder,
|
||||
loadStatistics,
|
||||
loadOrdersRequiringApproval,
|
||||
loadOverdueOrders,
|
||||
createPurchaseOrder,
|
||||
updateOrderStatus,
|
||||
approveOrder,
|
||||
sendToSupplier,
|
||||
cancelOrder,
|
||||
clearError,
|
||||
refresh,
|
||||
setPage
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DELIVERIES HOOK
|
||||
// ============================================================================
|
||||
|
||||
export interface UseDeliveries {
|
||||
deliveries: Delivery[];
|
||||
delivery: Delivery | null;
|
||||
todaysDeliveries: Delivery[];
|
||||
overdueDeliveries: Delivery[];
|
||||
performanceStats: DeliveryPerformanceStats | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
|
||||
loadDeliveries: (params?: DeliverySearchParams) => Promise<void>;
|
||||
loadDelivery: (deliveryId: string) => Promise<void>;
|
||||
loadTodaysDeliveries: () => Promise<void>;
|
||||
loadOverdueDeliveries: () => Promise<void>;
|
||||
loadPerformanceStats: (daysBack?: number, supplierId?: string) => Promise<void>;
|
||||
updateDeliveryStatus: (deliveryId: string, status: string, notes?: string) => Promise<Delivery | null>;
|
||||
receiveDelivery: (deliveryId: string, receiptData: any) => Promise<Delivery | null>;
|
||||
clearError: () => void;
|
||||
refresh: () => Promise<void>;
|
||||
setPage: (page: number) => void;
|
||||
}
|
||||
|
||||
export function useDeliveries(): UseDeliveries {
|
||||
const { user } = useAuth();
|
||||
|
||||
// State
|
||||
const [deliveries, setDeliveries] = useState<Delivery[]>([]);
|
||||
const [delivery, setDelivery] = useState<Delivery | null>(null);
|
||||
const [todaysDeliveries, setTodaysDeliveries] = useState<Delivery[]>([]);
|
||||
const [overdueDeliveries, setOverdueDeliveries] = useState<Delivery[]>([]);
|
||||
const [performanceStats, setPerformanceStats] = useState<DeliveryPerformanceStats | null>(null);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [currentParams, setCurrentParams] = useState<DeliverySearchParams>({});
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
});
|
||||
|
||||
// Load deliveries
|
||||
const loadDeliveries = useCallback(async (params: DeliverySearchParams = {}) => {
|
||||
if (!user?.tenant_id) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const searchParams = {
|
||||
...params,
|
||||
limit: pagination.limit,
|
||||
offset: ((params.offset !== undefined ? Math.floor(params.offset / pagination.limit) : pagination.page) - 1) * pagination.limit
|
||||
};
|
||||
|
||||
setCurrentParams(params);
|
||||
|
||||
const data = await suppliersService.getDeliveries(user.tenant_id, searchParams);
|
||||
setDeliveries(data);
|
||||
|
||||
// Update pagination
|
||||
const hasMore = data.length === pagination.limit;
|
||||
const currentPage = Math.floor((searchParams.offset || 0) / pagination.limit) + 1;
|
||||
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
page: currentPage,
|
||||
total: hasMore ? (currentPage * pagination.limit) + 1 : (currentPage - 1) * pagination.limit + data.length,
|
||||
totalPages: hasMore ? currentPage + 1 : currentPage
|
||||
}));
|
||||
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || err.message || 'Failed to load deliveries');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [user?.tenant_id, pagination.limit]);
|
||||
|
||||
const loadDelivery = useCallback(async (deliveryId: string) => {
|
||||
if (!user?.tenant_id) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = await suppliersService.getDelivery(user.tenant_id, deliveryId);
|
||||
setDelivery(data);
|
||||
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || err.message || 'Failed to load delivery');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [user?.tenant_id]);
|
||||
|
||||
const loadTodaysDeliveries = useCallback(async () => {
|
||||
if (!user?.tenant_id) return;
|
||||
|
||||
try {
|
||||
const data = await suppliersService.getTodaysDeliveries(user.tenant_id);
|
||||
setTodaysDeliveries(data);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load today\'s deliveries:', err);
|
||||
}
|
||||
}, [user?.tenant_id]);
|
||||
|
||||
const loadOverdueDeliveries = useCallback(async () => {
|
||||
if (!user?.tenant_id) return;
|
||||
|
||||
try {
|
||||
const data = await suppliersService.getOverdueDeliveries(user.tenant_id);
|
||||
setOverdueDeliveries(data);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load overdue deliveries:', err);
|
||||
}
|
||||
}, [user?.tenant_id]);
|
||||
|
||||
const loadPerformanceStats = useCallback(async (daysBack: number = 30, supplierId?: string) => {
|
||||
if (!user?.tenant_id) return;
|
||||
|
||||
try {
|
||||
const data = await suppliersService.getDeliveryPerformanceStats(user.tenant_id, daysBack, supplierId);
|
||||
setPerformanceStats(data);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load delivery performance stats:', err);
|
||||
}
|
||||
}, [user?.tenant_id]);
|
||||
|
||||
const updateDeliveryStatus = useCallback(async (deliveryId: string, status: string, notes?: string): Promise<Delivery | null> => {
|
||||
if (!user?.tenant_id || !user?.user_id) return null;
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
const updatedDelivery = await suppliersService.updateDeliveryStatus(user.tenant_id, user.user_id, deliveryId, status, notes);
|
||||
|
||||
if (delivery?.id === deliveryId) {
|
||||
setDelivery(updatedDelivery);
|
||||
}
|
||||
|
||||
await loadDeliveries(currentParams);
|
||||
|
||||
return updatedDelivery;
|
||||
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to update delivery status';
|
||||
setError(errorMessage);
|
||||
return null;
|
||||
}
|
||||
}, [user?.tenant_id, user?.user_id, delivery?.id, loadDeliveries, currentParams]);
|
||||
|
||||
const receiveDelivery = useCallback(async (deliveryId: string, receiptData: any): Promise<Delivery | null> => {
|
||||
if (!user?.tenant_id || !user?.user_id) return null;
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
const updatedDelivery = await suppliersService.receiveDelivery(user.tenant_id, user.user_id, deliveryId, receiptData);
|
||||
|
||||
if (delivery?.id === deliveryId) {
|
||||
setDelivery(updatedDelivery);
|
||||
}
|
||||
|
||||
await loadDeliveries(currentParams);
|
||||
|
||||
return updatedDelivery;
|
||||
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to receive delivery';
|
||||
setError(errorMessage);
|
||||
return null;
|
||||
}
|
||||
}, [user?.tenant_id, user?.user_id, delivery?.id, loadDeliveries, currentParams]);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await loadDeliveries(currentParams);
|
||||
if (todaysDeliveries.length > 0) await loadTodaysDeliveries();
|
||||
if (overdueDeliveries.length > 0) await loadOverdueDeliveries();
|
||||
if (performanceStats) await loadPerformanceStats();
|
||||
}, [currentParams, todaysDeliveries.length, overdueDeliveries.length, performanceStats, loadDeliveries, loadTodaysDeliveries, loadOverdueDeliveries, loadPerformanceStats]);
|
||||
|
||||
const setPage = useCallback((page: number) => {
|
||||
setPagination(prev => ({ ...prev, page }));
|
||||
const offset = (page - 1) * pagination.limit;
|
||||
loadDeliveries({ ...currentParams, offset });
|
||||
}, [pagination.limit, currentParams, loadDeliveries]);
|
||||
|
||||
return {
|
||||
deliveries,
|
||||
delivery,
|
||||
todaysDeliveries,
|
||||
overdueDeliveries,
|
||||
performanceStats,
|
||||
isLoading,
|
||||
error,
|
||||
pagination,
|
||||
|
||||
loadDeliveries,
|
||||
loadDelivery,
|
||||
loadTodaysDeliveries,
|
||||
loadOverdueDeliveries,
|
||||
loadPerformanceStats,
|
||||
updateDeliveryStatus,
|
||||
receiveDelivery,
|
||||
clearError,
|
||||
refresh,
|
||||
setPage
|
||||
};
|
||||
}
|
||||
@@ -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