/** * Inventory React Query hooks */ import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query'; import { inventoryService } from '../services/inventory'; import { transformationService } from '../services/transformations'; import { IngredientCreate, IngredientUpdate, IngredientResponse, StockCreate, StockUpdate, StockResponse, StockMovementCreate, StockMovementResponse, InventoryFilter, StockFilter, StockConsumptionRequest, StockConsumptionResponse, PaginatedResponse, ProductTransformationCreate, ProductTransformationResponse, ProductionStage, DeletionSummary, } from '../types/inventory'; import { ApiError } from '../client'; // Query Keys export const inventoryKeys = { all: ['inventory'] as const, ingredients: { all: () => [...inventoryKeys.all, 'ingredients'] as const, lists: () => [...inventoryKeys.ingredients.all(), 'list'] as const, list: (tenantId: string, filters?: InventoryFilter) => [...inventoryKeys.ingredients.lists(), tenantId, filters] as const, details: () => [...inventoryKeys.ingredients.all(), 'detail'] as const, detail: (tenantId: string, ingredientId: string) => [...inventoryKeys.ingredients.details(), tenantId, ingredientId] as const, byCategory: (tenantId: string) => [...inventoryKeys.ingredients.all(), 'by-category', tenantId] as const, lowStock: (tenantId: string) => [...inventoryKeys.ingredients.all(), 'low-stock', tenantId] as const, }, stock: { all: () => [...inventoryKeys.all, 'stock'] as const, lists: () => [...inventoryKeys.stock.all(), 'list'] as const, list: (tenantId: string, filters?: StockFilter) => [...inventoryKeys.stock.lists(), tenantId, filters] as const, details: () => [...inventoryKeys.stock.all(), 'detail'] as const, detail: (tenantId: string, stockId: string) => [...inventoryKeys.stock.details(), tenantId, stockId] as const, byIngredient: (tenantId: string, ingredientId: string, includeUnavailable?: boolean) => [...inventoryKeys.stock.all(), 'by-ingredient', tenantId, ingredientId, includeUnavailable] as const, expiring: (tenantId: string, withinDays?: number) => [...inventoryKeys.stock.all(), 'expiring', tenantId, withinDays] as const, expired: (tenantId: string) => [...inventoryKeys.stock.all(), 'expired', tenantId] as const, movements: (tenantId: string, ingredientId?: string) => [...inventoryKeys.stock.all(), 'movements', tenantId, ingredientId] as const, }, analytics: (tenantId: string, startDate?: string, endDate?: string) => [...inventoryKeys.all, 'analytics', tenantId, { startDate, endDate }] as const, transformations: { all: () => [...inventoryKeys.all, 'transformations'] as const, lists: () => [...inventoryKeys.transformations.all(), 'list'] as const, list: (tenantId: string, filters?: any) => [...inventoryKeys.transformations.lists(), tenantId, filters] as const, details: () => [...inventoryKeys.transformations.all(), 'detail'] as const, detail: (tenantId: string, transformationId: string) => [...inventoryKeys.transformations.details(), tenantId, transformationId] as const, summary: (tenantId: string, daysBack?: number) => [...inventoryKeys.transformations.all(), 'summary', tenantId, daysBack] as const, byIngredient: (tenantId: string, ingredientId: string) => [...inventoryKeys.transformations.all(), 'by-ingredient', tenantId, ingredientId] as const, byStage: (tenantId: string, sourceStage?: ProductionStage, targetStage?: ProductionStage) => [...inventoryKeys.transformations.all(), 'by-stage', tenantId, { sourceStage, targetStage }] as const, }, } as const; // Ingredient Queries export const useIngredients = ( tenantId: string, filter?: InventoryFilter, options?: Omit, 'queryKey' | 'queryFn'> ) => { return useQuery({ queryKey: inventoryKeys.ingredients.list(tenantId, filter), queryFn: () => inventoryService.getIngredients(tenantId, filter), enabled: !!tenantId, staleTime: 2 * 60 * 1000, // 2 minutes ...options, }); }; export const useIngredient = ( tenantId: string, ingredientId: string, options?: Omit, 'queryKey' | 'queryFn'> ) => { return useQuery({ queryKey: inventoryKeys.ingredients.detail(tenantId, ingredientId), queryFn: () => inventoryService.getIngredient(tenantId, ingredientId), enabled: !!tenantId && !!ingredientId, staleTime: 5 * 60 * 1000, // 5 minutes ...options, }); }; export const useIngredientsByCategory = ( tenantId: string, options?: Omit, ApiError>, 'queryKey' | 'queryFn'> ) => { return useQuery, ApiError>({ queryKey: inventoryKeys.ingredients.byCategory(tenantId), queryFn: () => inventoryService.getIngredientsByCategory(tenantId), enabled: !!tenantId, staleTime: 5 * 60 * 1000, // 5 minutes ...options, }); }; export const useLowStockIngredients = ( tenantId: string, options?: Omit, 'queryKey' | 'queryFn'> ) => { return useQuery({ queryKey: inventoryKeys.ingredients.lowStock(tenantId), queryFn: () => inventoryService.getLowStockIngredients(tenantId), enabled: !!tenantId, staleTime: 30 * 1000, // 30 seconds ...options, }); }; // Stock Queries export const useStock = ( tenantId: string, filter?: StockFilter, options?: Omit, ApiError>, 'queryKey' | 'queryFn'> ) => { return useQuery, ApiError>({ queryKey: inventoryKeys.stock.list(tenantId, filter), queryFn: () => inventoryService.getAllStock(tenantId, filter), enabled: !!tenantId, staleTime: 30 * 1000, // 30 seconds ...options, }); }; export const useStockByIngredient = ( tenantId: string, ingredientId: string, includeUnavailable: boolean = false, options?: Omit, 'queryKey' | 'queryFn'> ) => { return useQuery({ queryKey: inventoryKeys.stock.byIngredient(tenantId, ingredientId, includeUnavailable), queryFn: () => inventoryService.getStockByIngredient(tenantId, ingredientId, includeUnavailable), enabled: !!tenantId && !!ingredientId, staleTime: 1 * 60 * 1000, // 1 minute ...options, }); }; export const useExpiringStock = ( tenantId: string, withinDays: number = 7, options?: Omit, 'queryKey' | 'queryFn'> ) => { return useQuery({ queryKey: inventoryKeys.stock.expiring(tenantId, withinDays), queryFn: () => inventoryService.getExpiringStock(tenantId, withinDays), enabled: !!tenantId, staleTime: 30 * 1000, // 30 seconds ...options, }); }; export const useExpiredStock = ( tenantId: string, options?: Omit, 'queryKey' | 'queryFn'> ) => { return useQuery({ queryKey: inventoryKeys.stock.expired(tenantId), queryFn: () => inventoryService.getExpiredStock(tenantId), enabled: !!tenantId, staleTime: 1 * 60 * 1000, // 1 minute ...options, }); }; export const useStockMovements = ( tenantId: string, ingredientId?: string, limit: number = 50, offset: number = 0, options?: Omit, 'queryKey' | 'queryFn'> ) => { return useQuery({ queryKey: inventoryKeys.stock.movements(tenantId, ingredientId), queryFn: () => inventoryService.getStockMovements(tenantId, ingredientId, limit, offset), enabled: !!tenantId, staleTime: 1 * 60 * 1000, // 1 minute ...options, }); }; export const useStockAnalytics = ( tenantId: string, startDate?: string, endDate?: string, options?: Omit, 'queryKey' | 'queryFn'> ) => { return useQuery({ queryKey: inventoryKeys.analytics(tenantId, startDate, endDate), queryFn: () => inventoryService.getStockAnalytics(tenantId, startDate, endDate), enabled: !!tenantId, staleTime: 5 * 60 * 1000, // 5 minutes ...options, }); }; // Ingredient Mutations export const useCreateIngredient = ( options?: UseMutationOptions ) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ tenantId, ingredientData }) => inventoryService.createIngredient(tenantId, ingredientData), onSuccess: (data, { tenantId }) => { // Add to cache queryClient.setQueryData(inventoryKeys.ingredients.detail(tenantId, data.id), data); // Invalidate lists queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.byCategory(tenantId) }); }, ...options, }); }; export const useUpdateIngredient = ( options?: UseMutationOptions< IngredientResponse, ApiError, { tenantId: string; ingredientId: string; updateData: IngredientUpdate } > ) => { const queryClient = useQueryClient(); return useMutation< IngredientResponse, ApiError, { tenantId: string; ingredientId: string; updateData: IngredientUpdate } >({ mutationFn: ({ tenantId, ingredientId, updateData }) => inventoryService.updateIngredient(tenantId, ingredientId, updateData), onSuccess: (data, { tenantId, ingredientId }) => { // Update cache queryClient.setQueryData(inventoryKeys.ingredients.detail(tenantId, ingredientId), data); // Invalidate lists queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.byCategory(tenantId) }); }, ...options, }); }; export const useSoftDeleteIngredient = ( options?: UseMutationOptions ) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ tenantId, ingredientId }) => inventoryService.softDeleteIngredient(tenantId, ingredientId), onSuccess: (data, { tenantId, ingredientId }) => { // Remove from cache queryClient.removeQueries({ queryKey: inventoryKeys.ingredients.detail(tenantId, ingredientId) }); // Invalidate lists to reflect the soft deletion (item marked as inactive) queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.byCategory(tenantId) }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() }); }, ...options, }); }; export const useHardDeleteIngredient = ( options?: UseMutationOptions ) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ tenantId, ingredientId }) => inventoryService.hardDeleteIngredient(tenantId, ingredientId), onSuccess: (data, { tenantId, ingredientId }) => { // Remove from cache completely queryClient.removeQueries({ queryKey: inventoryKeys.ingredients.detail(tenantId, ingredientId) }); // Invalidate all related data since everything was deleted queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.byCategory(tenantId) }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId) }); queryClient.invalidateQueries({ queryKey: inventoryKeys.analytics.all() }); }, ...options, }); }; // Stock Mutations export const useAddStock = ( options?: UseMutationOptions ) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ tenantId, stockData }) => inventoryService.addStock(tenantId, stockData), onSuccess: (data, { tenantId }) => { // Invalidate stock queries queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.byIngredient(tenantId, data.ingredient_id) }); queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() }); }, ...options, }); }; export const useUpdateStock = ( options?: UseMutationOptions< StockResponse, ApiError, { tenantId: string; stockId: string; updateData: StockUpdate } > ) => { const queryClient = useQueryClient(); return useMutation< StockResponse, ApiError, { tenantId: string; stockId: string; updateData: StockUpdate } >({ mutationFn: ({ tenantId, stockId, updateData }) => inventoryService.updateStock(tenantId, stockId, updateData), onSuccess: (data, { tenantId, stockId }) => { // Update cache queryClient.setQueryData(inventoryKeys.stock.detail(tenantId, stockId), data); // Invalidate related queries queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.byIngredient(tenantId, data.ingredient_id) }); }, ...options, }); }; export const useConsumeStock = ( options?: UseMutationOptions< StockConsumptionResponse, ApiError, { tenantId: string; consumptionData: StockConsumptionRequest } > ) => { const queryClient = useQueryClient(); return useMutation< StockConsumptionResponse, ApiError, { tenantId: string; consumptionData: StockConsumptionRequest } >({ mutationFn: ({ tenantId, consumptionData }) => inventoryService.consumeStock(tenantId, consumptionData), onSuccess: (data, { tenantId, consumptionData }) => { // Invalidate stock queries for the affected ingredient queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.byIngredient(tenantId, consumptionData.ingredient_id) }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId) }); queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() }); }, ...options, }); }; export const useCreateStockMovement = ( options?: UseMutationOptions< StockMovementResponse, ApiError, { tenantId: string; movementData: StockMovementCreate } > ) => { const queryClient = useQueryClient(); return useMutation< StockMovementResponse, ApiError, { tenantId: string; movementData: StockMovementCreate } >({ mutationFn: ({ tenantId, movementData }) => inventoryService.createStockMovement(tenantId, movementData), onSuccess: (data, { tenantId, movementData }) => { // Invalidate movement queries queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId) }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId, movementData.ingredient_id) }); // Invalidate stock queries if this affects stock levels if (['in', 'out', 'adjustment'].includes(movementData.movement_type)) { queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.byIngredient(tenantId, movementData.ingredient_id) }); queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() }); } }, ...options, }); }; // Custom hooks for stock management operations export const useStockOperations = (tenantId: string) => { const queryClient = useQueryClient(); const addStock = useMutation({ mutationFn: async ({ ingredientId, quantity, unit_cost, notes }: { ingredientId: string; quantity: number; unit_cost?: number; notes?: string; }) => { // Create stock entry via backend API const stockData: StockCreate = { ingredient_id: ingredientId, quantity, unit_price: unit_cost || 0, notes }; return inventoryService.addStock(tenantId, stockData); }, onSuccess: (data, variables) => { // Invalidate relevant queries queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId) }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId, variables.ingredientId) }); } }); const consumeStock = useMutation({ mutationFn: async ({ ingredientId, quantity, reference_number, notes, fifo = true }: { ingredientId: string; quantity: number; reference_number?: string; notes?: string; fifo?: boolean; }) => { const consumptionData: StockConsumptionRequest = { ingredient_id: ingredientId, quantity, reference_number, notes, fifo }; return inventoryService.consumeStock(tenantId, consumptionData); }, onSuccess: (data, variables) => { // Invalidate relevant queries queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId) }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId, variables.ingredientId) }); } }); const adjustStock = useMutation({ mutationFn: async ({ ingredientId, quantity, notes }: { ingredientId: string; quantity: number; notes?: string; }) => { // Create adjustment movement via backend API const movementData: StockMovementCreate = { ingredient_id: ingredientId, movement_type: 'adjustment', quantity, notes }; return inventoryService.createStockMovement(tenantId, movementData); }, onSuccess: (data, variables) => { // Invalidate relevant queries queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId) }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId, variables.ingredientId) }); } }); return { addStock, consumeStock, adjustStock }; }; // ===== TRANSFORMATION HOOKS ===== export const useTransformations = ( tenantId: string, options?: { skip?: number; limit?: number; ingredient_id?: string; source_stage?: ProductionStage; target_stage?: ProductionStage; days_back?: number; }, queryOptions?: Omit, 'queryKey' | 'queryFn'> ) => { return useQuery({ queryKey: inventoryKeys.transformations.list(tenantId, options), queryFn: () => transformationService.getTransformations(tenantId, options), enabled: !!tenantId, staleTime: 1 * 60 * 1000, // 1 minute ...queryOptions, }); }; export const useTransformation = ( tenantId: string, transformationId: string, options?: Omit, 'queryKey' | 'queryFn'> ) => { return useQuery({ queryKey: inventoryKeys.transformations.detail(tenantId, transformationId), queryFn: () => transformationService.getTransformation(tenantId, transformationId), enabled: !!tenantId && !!transformationId, staleTime: 2 * 60 * 1000, // 2 minutes ...options, }); }; export const useTransformationSummary = ( tenantId: string, daysBack: number = 30, options?: Omit, 'queryKey' | 'queryFn'> ) => { return useQuery({ queryKey: inventoryKeys.transformations.summary(tenantId, daysBack), queryFn: () => transformationService.getTransformationSummary(tenantId, daysBack), enabled: !!tenantId, staleTime: 5 * 60 * 1000, // 5 minutes ...options, }); }; export const useTransformationsByIngredient = ( tenantId: string, ingredientId: string, limit: number = 50, options?: Omit, 'queryKey' | 'queryFn'> ) => { return useQuery({ queryKey: inventoryKeys.transformations.byIngredient(tenantId, ingredientId), queryFn: () => transformationService.getTransformationsForIngredient(tenantId, ingredientId, limit), enabled: !!tenantId && !!ingredientId, staleTime: 2 * 60 * 1000, // 2 minutes ...options, }); }; export const useTransformationsByStage = ( tenantId: string, sourceStage?: ProductionStage, targetStage?: ProductionStage, limit: number = 50, options?: Omit, 'queryKey' | 'queryFn'> ) => { return useQuery({ queryKey: inventoryKeys.transformations.byStage(tenantId, sourceStage, targetStage), queryFn: () => transformationService.getTransformationsByStage(tenantId, sourceStage, targetStage, limit), enabled: !!tenantId, staleTime: 2 * 60 * 1000, // 2 minutes ...options, }); }; // ===== TRANSFORMATION MUTATIONS ===== export const useCreateTransformation = ( options?: UseMutationOptions< ProductTransformationResponse, ApiError, { tenantId: string; transformationData: ProductTransformationCreate } > ) => { const queryClient = useQueryClient(); return useMutation< ProductTransformationResponse, ApiError, { tenantId: string; transformationData: ProductTransformationCreate } >({ mutationFn: ({ tenantId, transformationData }) => transformationService.createTransformation(tenantId, transformationData), onSuccess: (data, { tenantId, transformationData }) => { // Add to cache queryClient.setQueryData( inventoryKeys.transformations.detail(tenantId, data.id), data ); // Invalidate related queries queryClient.invalidateQueries({ queryKey: inventoryKeys.transformations.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId) }); // Invalidate ingredient-specific queries queryClient.invalidateQueries({ queryKey: inventoryKeys.transformations.byIngredient(tenantId, transformationData.source_ingredient_id) }); queryClient.invalidateQueries({ queryKey: inventoryKeys.transformations.byIngredient(tenantId, transformationData.target_ingredient_id) }); }, ...options, }); }; export const useParBakeTransformation = ( options?: UseMutationOptions< any, ApiError, { tenantId: string; source_ingredient_id: string; target_ingredient_id: string; quantity: number; target_batch_number?: string; expiration_hours?: number; notes?: string; } > ) => { const queryClient = useQueryClient(); return useMutation< any, ApiError, { tenantId: string; source_ingredient_id: string; target_ingredient_id: string; quantity: number; target_batch_number?: string; expiration_hours?: number; notes?: string; } >({ mutationFn: ({ tenantId, ...transformationOptions }) => transformationService.createParBakeToFreshTransformation(tenantId, transformationOptions), onSuccess: (data, { tenantId, source_ingredient_id, target_ingredient_id }) => { // Invalidate related queries queryClient.invalidateQueries({ queryKey: inventoryKeys.transformations.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() }); queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId) }); // Invalidate ingredient-specific queries queryClient.invalidateQueries({ queryKey: inventoryKeys.transformations.byIngredient(tenantId, source_ingredient_id) }); queryClient.invalidateQueries({ queryKey: inventoryKeys.transformations.byIngredient(tenantId, target_ingredient_id) }); }, ...options, }); }; // Custom hook for common transformation operations export const useTransformationOperations = (tenantId: string) => { const createTransformation = useCreateTransformation(); const parBakeTransformation = useParBakeTransformation(); const bakeParBakedCroissants = useMutation({ mutationFn: async ({ parBakedIngredientId, freshBakedIngredientId, quantity, expirationHours = 24, notes, }: { parBakedIngredientId: string; freshBakedIngredientId: string; quantity: number; expirationHours?: number; notes?: string; }) => { return transformationService.bakeParBakedCroissants( tenantId, parBakedIngredientId, freshBakedIngredientId, quantity, expirationHours, notes ); }, onSuccess: () => { // Invalidate related queries createTransformation.reset(); }, }); const transformFrozenToPrepared = useMutation({ mutationFn: async ({ frozenIngredientId, preparedIngredientId, quantity, notes, }: { frozenIngredientId: string; preparedIngredientId: string; quantity: number; notes?: string; }) => { return transformationService.transformFrozenToPrepared( tenantId, frozenIngredientId, preparedIngredientId, quantity, notes ); }, onSuccess: () => { // Invalidate related queries createTransformation.reset(); }, }); return { createTransformation, parBakeTransformation, bakeParBakedCroissants, transformFrozenToPrepared, }; };