890 lines
29 KiB
TypeScript
890 lines
29 KiB
TypeScript
|
|
// 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
|
||
|
|
};
|
||
|
|
}
|