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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user