Create new services: inventory, recipes, suppliers

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

View File

@@ -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';

View 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
};
};

View 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
};
};

View 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
};
}