From 93c9475239286b8d730015fb323643b90c8950c1 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Thu, 1 Jan 2026 19:01:33 +0100 Subject: [PATCH] fix UI 1 --- frontend/src/api/hooks/inventory.ts | 27 ++- frontend/src/api/services/inventory.ts | 11 ++ frontend/src/api/types/inventory.ts | 22 +++ .../setup-wizard/steps/InventorySetupStep.tsx | 174 +++++++++++------- frontend/src/pages/public/DemoPage.tsx | 11 +- frontend/src/router/routes.config.ts | 2 +- .../app/services/session_manager.py | 6 +- services/inventory/app/api/stock_entries.py | 35 +++- services/inventory/app/schemas/inventory.py | 24 +++ .../app/services/inventory_service.py | 58 +++++- 10 files changed, 286 insertions(+), 84 deletions(-) diff --git a/frontend/src/api/hooks/inventory.ts b/frontend/src/api/hooks/inventory.ts index 1ec1da58..db17e706 100644 --- a/frontend/src/api/hooks/inventory.ts +++ b/frontend/src/api/hooks/inventory.ts @@ -23,6 +23,7 @@ import { ProductTransformationResponse, ProductionStage, DeletionSummary, + BulkStockResponse, } from '../types/inventory'; import { ApiError } from '../client'; @@ -342,7 +343,7 @@ export const useAddStock = ( options?: UseMutationOptions ) => { const queryClient = useQueryClient(); - + return useMutation({ mutationFn: ({ tenantId, stockData }) => inventoryService.addStock(tenantId, stockData), onSuccess: (data, { tenantId }) => { @@ -355,6 +356,30 @@ export const useAddStock = ( }); }; +export const useBulkAddStock = ( + options?: UseMutationOptions +) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ tenantId, stocks }) => inventoryService.bulkAddStock(tenantId, stocks), + onSuccess: (data, { tenantId }) => { + // Invalidate all stock queries since multiple ingredients may have been affected + queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() }); + queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() }); + // Invalidate per-ingredient stock queries + data.results.forEach((result) => { + if (result.success && result.stock) { + queryClient.invalidateQueries({ + queryKey: inventoryKeys.stock.byIngredient(tenantId, result.stock.ingredient_id) + }); + } + }); + }, + ...options, + }); +}; + export const useUpdateStock = ( options?: UseMutationOptions< StockResponse, diff --git a/frontend/src/api/services/inventory.ts b/frontend/src/api/services/inventory.ts index 668acb89..70d97109 100644 --- a/frontend/src/api/services/inventory.ts +++ b/frontend/src/api/services/inventory.ts @@ -29,6 +29,7 @@ import { StockFilter, StockMovementCreate, StockMovementResponse, + BulkStockResponse, // Operations StockConsumptionRequest, StockConsumptionResponse, @@ -162,6 +163,16 @@ export class InventoryService { ); } + async bulkAddStock( + tenantId: string, + stocks: StockCreate[] + ): Promise { + return apiClient.post( + `${this.baseUrl}/${tenantId}/inventory/stock/bulk`, + { stocks } + ); + } + async getStock(tenantId: string, stockId: string): Promise { return apiClient.get( `${this.baseUrl}/${tenantId}/inventory/stock/${stockId}` diff --git a/frontend/src/api/types/inventory.ts b/frontend/src/api/types/inventory.ts index d5565bcb..a71ea503 100644 --- a/frontend/src/api/types/inventory.ts +++ b/frontend/src/api/types/inventory.ts @@ -330,6 +330,28 @@ export interface StockResponse { ingredient?: IngredientResponse | null; } +// ===== BULK STOCK SCHEMAS ===== +// Mirror: BulkStockCreate, BulkStockResult, BulkStockResponse from inventory.py + +export interface BulkStockCreate { + stocks: StockCreate[]; +} + +export interface BulkStockResult { + index: number; + success: boolean; + stock: StockResponse | null; + error: string | null; +} + +export interface BulkStockResponse { + total_requested: number; + total_created: number; + total_failed: number; + results: BulkStockResult[]; + transaction_id: string; +} + // ===== STOCK MOVEMENT SCHEMAS ===== // Mirror: StockMovementCreate from inventory.py:277 diff --git a/frontend/src/components/domain/setup-wizard/steps/InventorySetupStep.tsx b/frontend/src/components/domain/setup-wizard/steps/InventorySetupStep.tsx index 37555876..c76b12d2 100644 --- a/frontend/src/components/domain/setup-wizard/steps/InventorySetupStep.tsx +++ b/frontend/src/components/domain/setup-wizard/steps/InventorySetupStep.tsx @@ -1,12 +1,12 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { SetupStepProps } from '../types'; -import { useIngredients, useCreateIngredient, useUpdateIngredient, useSoftDeleteIngredient, useAddStock, useStockByIngredient } from '../../../../api/hooks/inventory'; +import { useIngredients, useCreateIngredient, useUpdateIngredient, useSoftDeleteIngredient, useBulkAddStock } from '../../../../api/hooks/inventory'; import { useSuppliers } from '../../../../api/hooks/suppliers'; import { useCurrentTenant } from '../../../../stores/tenant.store'; import { useAuthUser } from '../../../../stores/auth.store'; import { UnitOfMeasure, IngredientCategory, ProductionStage } from '../../../../api/types/inventory'; -import type { IngredientCreate, IngredientUpdate, StockCreate, StockResponse } from '../../../../api/types/inventory'; +import type { IngredientCreate, IngredientUpdate, StockCreate } from '../../../../api/types/inventory'; import { ESSENTIAL_INGREDIENTS, COMMON_INGREDIENTS, PACKAGING_ITEMS, type IngredientTemplate, templateToIngredientCreate } from '../data/ingredientTemplates'; export const InventorySetupStep: React.FC = ({ onUpdate, onComplete, onPrevious, canContinue }) => { @@ -29,7 +29,7 @@ export const InventorySetupStep: React.FC = ({ onUpdate, onCompl const createIngredientMutation = useCreateIngredient(); const updateIngredientMutation = useUpdateIngredient(); const deleteIngredientMutation = useSoftDeleteIngredient(); - const addStockMutation = useAddStock(); + const bulkAddStockMutation = useBulkAddStock(); // Form state const [isAdding, setIsAdding] = useState(false); @@ -59,8 +59,10 @@ export const InventorySetupStep: React.FC = ({ onUpdate, onCompl }); const [stockErrors, setStockErrors] = useState>({}); - // Track stocks added per ingredient (for displaying the list) - const [ingredientStocks, setIngredientStocks] = useState>({}); + // Track pending stocks to be submitted in batch (for display and batch submission) + const [pendingStocks, setPendingStocks] = useState([]); + // Track stocks display per ingredient (using StockCreate for pending, before API submission) + const [ingredientStocks, setIngredientStocks] = useState>>({}); // Notify parent when count changes useEffect(() => { @@ -270,55 +272,79 @@ export const InventorySetupStep: React.FC = ({ onUpdate, onCompl const handleSaveStock = async (addAnother: boolean = false) => { if (!addingStockForId || !validateStockForm()) return; - try { - const stockData: StockCreate = { - ingredient_id: addingStockForId, - current_quantity: Number(stockFormData.current_quantity), - expiration_date: stockFormData.expiration_date || undefined, - supplier_id: stockFormData.supplier_id || undefined, - batch_number: stockFormData.batch_number || undefined, - lot_number: stockFormData.lot_number || undefined, - production_stage: ProductionStage.RAW_INGREDIENT, - quality_status: 'good', - }; + const stockData: StockCreate = { + ingredient_id: addingStockForId, + current_quantity: Number(stockFormData.current_quantity), + expiration_date: stockFormData.expiration_date || undefined, + supplier_id: stockFormData.supplier_id || undefined, + batch_number: stockFormData.batch_number || undefined, + lot_number: stockFormData.lot_number || undefined, + production_stage: ProductionStage.RAW_INGREDIENT, + quality_status: 'good', + }; - const result = await addStockMutation.mutateAsync({ - tenantId, - stockData, + // Generate a temporary ID for display purposes + const tempId = `temp-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; + const stockWithTempId = { ...stockData, _tempId: tempId }; + + // Add to pending stocks for batch submission + setPendingStocks(prev => [...prev, stockData]); + + // Add to local state for display + setIngredientStocks(prev => ({ + ...prev, + [addingStockForId]: [...(prev[addingStockForId] || []), stockWithTempId], + })); + + if (addAnother) { + // Reset form for adding another lot + setStockFormData({ + current_quantity: '', + expiration_date: '', + supplier_id: stockFormData.supplier_id, // Keep supplier selected + batch_number: '', + lot_number: '', }); - - // Add to local state for display - setIngredientStocks(prev => ({ - ...prev, - [addingStockForId]: [...(prev[addingStockForId] || []), result], - })); - - if (addAnother) { - // Reset form for adding another lot - setStockFormData({ - current_quantity: '', - expiration_date: '', - supplier_id: stockFormData.supplier_id, // Keep supplier selected - batch_number: '', - lot_number: '', - }); - setStockErrors({}); - } else { - handleCancelStock(); - } - } catch (error) { - console.error('Error adding stock:', error); - setStockErrors({ submit: t('common:error_saving', 'Error saving. Please try again.') }); + setStockErrors({}); + } else { + handleCancelStock(); } }; - const handleDeleteStock = async (ingredientId: string, stockId: string) => { - // Remove from local state + const handleDeleteStock = (ingredientId: string, tempId: string) => { + // Remove from local display state setIngredientStocks(prev => ({ ...prev, - [ingredientId]: (prev[ingredientId] || []).filter(s => s.id !== stockId), + [ingredientId]: (prev[ingredientId] || []).filter(s => s._tempId !== tempId), })); - // Note: We don't delete from backend during setup - stocks are created and can be managed later + // Remove from pending stocks + setPendingStocks(prev => prev.filter((s) => { + // Find and remove the matching stock entry + const stocksForIngredient = ingredientStocks[ingredientId] || []; + const stockToRemove = stocksForIngredient.find(st => st._tempId === tempId); + if (!stockToRemove) return true; + return s.ingredient_id !== stockToRemove.ingredient_id || + s.current_quantity !== stockToRemove.current_quantity || + s.batch_number !== stockToRemove.batch_number; + })); + }; + + // Submit all pending stocks when proceeding to next step + const handleSubmitPendingStocks = async (): Promise => { + if (pendingStocks.length === 0) return true; + + try { + await bulkAddStockMutation.mutateAsync({ + tenantId, + stocks: pendingStocks, + }); + // Clear pending stocks after successful submission + setPendingStocks([]); + return true; + } catch (error) { + console.error('Error submitting stocks:', error); + return false; + } }; const categoryOptions = [ @@ -640,7 +666,7 @@ export const InventorySetupStep: React.FC = ({ onUpdate, onCompl
{stocks.map((stock) => (
@@ -659,7 +685,7 @@ export const InventorySetupStep: React.FC = ({ onUpdate, onCompl
@@ -1021,13 +1033,33 @@ export const InventorySetupStep: React.FC = ({ onUpdate, onCompl
+ {pendingStocks.length > 0 && ( + + {pendingStocks.length} {t('setup_wizard:inventory.pending_stocks', 'stock entries pending')} + + )}
diff --git a/frontend/src/pages/public/DemoPage.tsx b/frontend/src/pages/public/DemoPage.tsx index 35c66c32..f69c6d64 100644 --- a/frontend/src/pages/public/DemoPage.tsx +++ b/frontend/src/pages/public/DemoPage.tsx @@ -79,10 +79,10 @@ const DemoPage = () => { // Helper function to calculate estimated progress based on elapsed time const calculateEstimatedProgress = (tier: string, startTime: number): number => { const elapsed = Date.now() - startTime; - const duration = tier === 'enterprise' ? 90000 : 40000; // ms (90s for enterprise, 40s for professional) + const duration = 5000; // ms (5s for both professional and enterprise) const linearProgress = Math.min(95, (elapsed / duration) * 100); // Logarithmic curve for natural feel - starts fast, slows down - return Math.min(95, Math.round(linearProgress * (1 - Math.exp(-elapsed / 10000)))); + return Math.min(95, Math.round(linearProgress * (1 - Math.exp(-elapsed / 1000)))); }; const demoOptions = [ @@ -645,6 +645,7 @@ const DemoPage = () => {
{
{/* Card Body */} -
+
{/* Features List with Icons */}

@@ -765,7 +766,7 @@ const DemoPage = () => {

{/* Card Footer */} -
+