2025-09-05 17:49:48 +02:00
|
|
|
/**
|
|
|
|
|
* Inventory React Query hooks
|
|
|
|
|
*/
|
|
|
|
|
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
|
|
|
|
import { inventoryService } from '../services/inventory';
|
2025-09-17 16:06:30 +02:00
|
|
|
import { transformationService } from '../services/transformations';
|
2025-09-05 17:49:48 +02:00
|
|
|
import {
|
|
|
|
|
IngredientCreate,
|
|
|
|
|
IngredientUpdate,
|
|
|
|
|
IngredientResponse,
|
|
|
|
|
StockCreate,
|
|
|
|
|
StockUpdate,
|
|
|
|
|
StockResponse,
|
|
|
|
|
StockMovementCreate,
|
|
|
|
|
StockMovementResponse,
|
|
|
|
|
InventoryFilter,
|
|
|
|
|
StockFilter,
|
|
|
|
|
StockConsumptionRequest,
|
|
|
|
|
StockConsumptionResponse,
|
|
|
|
|
PaginatedResponse,
|
2025-09-17 16:06:30 +02:00
|
|
|
ProductTransformationCreate,
|
|
|
|
|
ProductTransformationResponse,
|
|
|
|
|
ProductionStage,
|
|
|
|
|
DeletionSummary,
|
2025-09-05 17:49:48 +02:00
|
|
|
} 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,
|
|
|
|
|
},
|
2025-09-17 16:06:30 +02:00
|
|
|
analytics: (tenantId: string, startDate?: string, endDate?: string) =>
|
2025-09-05 17:49:48 +02:00
|
|
|
[...inventoryKeys.all, 'analytics', tenantId, { startDate, endDate }] as const,
|
2025-09-17 16:06:30 +02:00
|
|
|
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,
|
|
|
|
|
},
|
2025-09-05 17:49:48 +02:00
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
// Ingredient Queries
|
|
|
|
|
export const useIngredients = (
|
|
|
|
|
tenantId: string,
|
|
|
|
|
filter?: InventoryFilter,
|
2025-09-09 22:27:52 +02:00
|
|
|
options?: Omit<UseQueryOptions<IngredientResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
2025-09-05 17:49:48 +02:00
|
|
|
) => {
|
2025-09-09 22:27:52 +02:00
|
|
|
return useQuery<IngredientResponse[], ApiError>({
|
2025-09-05 17:49:48 +02:00
|
|
|
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<UseQueryOptions<IngredientResponse, ApiError>, 'queryKey' | 'queryFn'>
|
|
|
|
|
) => {
|
|
|
|
|
return useQuery<IngredientResponse, ApiError>({
|
|
|
|
|
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<UseQueryOptions<Record<string, IngredientResponse[]>, ApiError>, 'queryKey' | 'queryFn'>
|
|
|
|
|
) => {
|
|
|
|
|
return useQuery<Record<string, IngredientResponse[]>, 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<UseQueryOptions<IngredientResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
|
|
|
|
) => {
|
|
|
|
|
return useQuery<IngredientResponse[], ApiError>({
|
|
|
|
|
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<UseQueryOptions<PaginatedResponse<StockResponse>, ApiError>, 'queryKey' | 'queryFn'>
|
|
|
|
|
) => {
|
|
|
|
|
return useQuery<PaginatedResponse<StockResponse>, 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<UseQueryOptions<StockResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
|
|
|
|
) => {
|
|
|
|
|
return useQuery<StockResponse[], ApiError>({
|
|
|
|
|
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<UseQueryOptions<StockResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
|
|
|
|
) => {
|
|
|
|
|
return useQuery<StockResponse[], ApiError>({
|
|
|
|
|
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<UseQueryOptions<StockResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
|
|
|
|
) => {
|
|
|
|
|
return useQuery<StockResponse[], ApiError>({
|
|
|
|
|
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,
|
2025-09-16 12:21:15 +02:00
|
|
|
options?: Omit<UseQueryOptions<StockMovementResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
2025-09-05 17:49:48 +02:00
|
|
|
) => {
|
2025-09-16 12:21:15 +02:00
|
|
|
return useQuery<StockMovementResponse[], ApiError>({
|
2025-09-05 17:49:48 +02:00
|
|
|
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<UseQueryOptions<any, ApiError>, 'queryKey' | 'queryFn'>
|
|
|
|
|
) => {
|
|
|
|
|
return useQuery<any, ApiError>({
|
|
|
|
|
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<IngredientResponse, ApiError, { tenantId: string; ingredientData: IngredientCreate }>
|
|
|
|
|
) => {
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
|
|
|
|
return useMutation<IngredientResponse, ApiError, { tenantId: string; ingredientData: IngredientCreate }>({
|
|
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-17 16:06:30 +02:00
|
|
|
export const useSoftDeleteIngredient = (
|
|
|
|
|
options?: UseMutationOptions<void, ApiError, { tenantId: string; ingredientId: string }>
|
2025-09-05 17:49:48 +02:00
|
|
|
) => {
|
|
|
|
|
const queryClient = useQueryClient();
|
2025-09-17 16:06:30 +02:00
|
|
|
|
|
|
|
|
return useMutation<void, ApiError, { tenantId: string; ingredientId: string }>({
|
|
|
|
|
mutationFn: ({ tenantId, ingredientId }) => inventoryService.softDeleteIngredient(tenantId, ingredientId),
|
2025-09-05 17:49:48 +02:00
|
|
|
onSuccess: (data, { tenantId, ingredientId }) => {
|
|
|
|
|
// Remove from cache
|
|
|
|
|
queryClient.removeQueries({ queryKey: inventoryKeys.ingredients.detail(tenantId, ingredientId) });
|
2025-09-17 16:06:30 +02:00
|
|
|
// 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<DeletionSummary, ApiError, { tenantId: string; ingredientId: string }>
|
|
|
|
|
) => {
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
|
|
|
|
return useMutation<DeletionSummary, ApiError, { tenantId: string; ingredientId: string }>({
|
|
|
|
|
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
|
2025-09-05 17:49:48 +02:00
|
|
|
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.byCategory(tenantId) });
|
2025-09-17 16:06:30 +02:00
|
|
|
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.movements(tenantId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: inventoryKeys.analytics.all() });
|
2025-09-05 17:49:48 +02:00
|
|
|
},
|
|
|
|
|
...options,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Stock Mutations
|
|
|
|
|
export const useAddStock = (
|
|
|
|
|
options?: UseMutationOptions<StockResponse, ApiError, { tenantId: string; stockData: StockCreate }>
|
|
|
|
|
) => {
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
|
|
|
|
return useMutation<StockResponse, ApiError, { tenantId: string; stockData: StockCreate }>({
|
|
|
|
|
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<
|
2025-09-16 12:21:15 +02:00
|
|
|
StockMovementResponse,
|
|
|
|
|
ApiError,
|
2025-09-05 17:49:48 +02:00
|
|
|
{ tenantId: string; movementData: StockMovementCreate }
|
|
|
|
|
>
|
|
|
|
|
) => {
|
|
|
|
|
const queryClient = useQueryClient();
|
2025-09-16 12:21:15 +02:00
|
|
|
|
2025-09-05 17:49:48 +02:00
|
|
|
return useMutation<
|
2025-09-16 12:21:15 +02:00
|
|
|
StockMovementResponse,
|
|
|
|
|
ApiError,
|
2025-09-05 17:49:48 +02:00
|
|
|
{ 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) });
|
2025-09-16 12:21:15 +02:00
|
|
|
queryClient.invalidateQueries({
|
|
|
|
|
queryKey: inventoryKeys.stock.movements(tenantId, movementData.ingredient_id)
|
2025-09-05 17:49:48 +02:00
|
|
|
});
|
|
|
|
|
// Invalidate stock queries if this affects stock levels
|
|
|
|
|
if (['in', 'out', 'adjustment'].includes(movementData.movement_type)) {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() });
|
2025-09-16 12:21:15 +02:00
|
|
|
queryClient.invalidateQueries({
|
|
|
|
|
queryKey: inventoryKeys.stock.byIngredient(tenantId, movementData.ingredient_id)
|
2025-09-05 17:49:48 +02:00
|
|
|
});
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() });
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
...options,
|
|
|
|
|
});
|
2025-09-16 12:21:15 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
};
|
2025-09-17 16:06:30 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ===== 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<UseQueryOptions<ProductTransformationResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
|
|
|
|
) => {
|
|
|
|
|
return useQuery<ProductTransformationResponse[], ApiError>({
|
|
|
|
|
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<UseQueryOptions<ProductTransformationResponse, ApiError>, 'queryKey' | 'queryFn'>
|
|
|
|
|
) => {
|
|
|
|
|
return useQuery<ProductTransformationResponse, ApiError>({
|
|
|
|
|
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<UseQueryOptions<any, ApiError>, 'queryKey' | 'queryFn'>
|
|
|
|
|
) => {
|
|
|
|
|
return useQuery<any, ApiError>({
|
|
|
|
|
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<UseQueryOptions<ProductTransformationResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
|
|
|
|
) => {
|
|
|
|
|
return useQuery<ProductTransformationResponse[], ApiError>({
|
|
|
|
|
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<UseQueryOptions<ProductTransformationResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
|
|
|
|
) => {
|
|
|
|
|
return useQuery<ProductTransformationResponse[], ApiError>({
|
|
|
|
|
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,
|
|
|
|
|
};
|
2025-09-05 17:49:48 +02:00
|
|
|
};
|