Improve the inventory page

This commit is contained in:
Urtzi Alfaro
2025-09-17 16:06:30 +02:00
parent 7aa26d51d3
commit dcb3ce441b
39 changed files with 5852 additions and 1762 deletions

View File

@@ -3,6 +3,7 @@
*/
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
import { inventoryService } from '../services/inventory';
import { transformationService } from '../services/transformations';
import {
IngredientCreate,
IngredientUpdate,
@@ -17,6 +18,10 @@ import {
StockConsumptionRequest,
StockConsumptionResponse,
PaginatedResponse,
ProductTransformationCreate,
ProductTransformationResponse,
ProductionStage,
DeletionSummary,
} from '../types/inventory';
import { ApiError } from '../client';
@@ -53,8 +58,23 @@ export const inventoryKeys = {
movements: (tenantId: string, ingredientId?: string) =>
[...inventoryKeys.stock.all(), 'movements', tenantId, ingredientId] as const,
},
analytics: (tenantId: string, startDate?: string, endDate?: string) =>
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
@@ -246,19 +266,41 @@ export const useUpdateIngredient = (
});
};
export const useDeleteIngredient = (
options?: UseMutationOptions<{ message: string }, ApiError, { tenantId: string; ingredientId: string }>
export const useSoftDeleteIngredient = (
options?: UseMutationOptions<void, ApiError, { tenantId: string; ingredientId: string }>
) => {
const queryClient = useQueryClient();
return useMutation<{ message: string }, ApiError, { tenantId: string; ingredientId: string }>({
mutationFn: ({ tenantId, ingredientId }) => inventoryService.deleteIngredient(tenantId, ingredientId),
return useMutation<void, ApiError, { tenantId: string; ingredientId: string }>({
mutationFn: ({ tenantId, ingredientId }) => inventoryService.softDeleteIngredient(tenantId, ingredientId),
onSuccess: (data, { tenantId, ingredientId }) => {
// Remove from cache
queryClient.removeQueries({ queryKey: inventoryKeys.ingredients.detail(tenantId, ingredientId) });
// Invalidate lists
// 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
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,
});
@@ -452,4 +494,245 @@ export const useStockOperations = (tenantId: string) => {
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<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,
};
};