From bfa5ff06377544b354c52c4bf9c393c4d9bbaa62 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Fri, 19 Dec 2025 13:10:24 +0100 Subject: [PATCH] Imporve onboarding UI --- docs/wizard-flow-specification.md | 2 +- .../dashboard/CollapsibleSetupBanner.tsx | 2 +- .../onboarding/UnifiedOnboardingWizard.tsx | 179 ++++++++++++---- .../steps/BakeryTypeSelectionStep.tsx | 114 +++++----- .../onboarding/steps/CompletionStep.tsx | 105 ++++----- .../steps/InitialStockEntryStep.tsx | 70 ++++-- .../onboarding/steps/InventoryReviewStep.tsx | 201 +++++++++++------- .../onboarding/steps/RegisterTenantStep.tsx | 179 +++++++++++----- .../components/StepNavigation.tsx | 2 +- .../setup-wizard/steps/InventorySetupStep.tsx | 49 +++-- .../setup-wizard/steps/QualitySetupStep.tsx | 66 +++--- .../setup-wizard/steps/RecipesSetupStep.tsx | 59 ++--- .../setup-wizard/steps/ReviewSetupStep.tsx | 41 ++-- .../setup-wizard/steps/SuppliersSetupStep.tsx | 22 +- .../setup-wizard/steps/TeamSetupStep.tsx | 52 +++-- .../components/domain/setup-wizard/types.ts | 11 + frontend/src/locales/en/common.json | 2 +- frontend/src/locales/en/dashboard.json | 4 +- frontend/src/locales/en/help.json | 4 +- frontend/src/locales/en/onboarding.json | 2 +- frontend/src/locales/en/setup_wizard.json | 24 +-- frontend/src/locales/en/suppliers.json | 2 +- frontend/src/locales/es/auth.json | 2 +- frontend/src/locales/es/common.json | 2 +- frontend/src/locales/es/dashboard.json | 12 +- frontend/src/locales/es/help.json | 4 +- frontend/src/locales/es/inventory.json | 2 +- frontend/src/locales/es/onboarding.json | 2 +- frontend/src/locales/es/setup_wizard.json | 24 +-- frontend/src/locales/es/suppliers.json | 2 +- frontend/src/locales/eu/auth.json | 2 +- frontend/src/locales/eu/dashboard.json | 4 +- frontend/src/locales/eu/help.json | 4 +- frontend/src/locales/eu/onboarding.json | 4 +- frontend/src/locales/eu/setup_wizard.json | 24 +-- frontend/src/locales/eu/suppliers.json | 2 +- frontend/src/pages/app/DashboardPage.tsx | 35 ++- frontend/src/styles/animations.css | 59 +++++ services/auth/app/api/onboarding_progress.py | 123 ++++++++--- 39 files changed, 1016 insertions(+), 483 deletions(-) create mode 100644 frontend/src/components/domain/setup-wizard/types.ts diff --git a/docs/wizard-flow-specification.md b/docs/wizard-flow-specification.md index deca611b..7685d013 100644 --- a/docs/wizard-flow-specification.md +++ b/docs/wizard-flow-specification.md @@ -1844,7 +1844,7 @@ completed_at: TIMESTAMP (nullable) "suppliers": { "title": "Add Suppliers", "description": "Your ingredient and material providers", - "min_required": "Add at least {{count}} supplier to continue", + "min_required": "Add at least {count} supplier to continue", ... } }, diff --git a/frontend/src/components/dashboard/CollapsibleSetupBanner.tsx b/frontend/src/components/dashboard/CollapsibleSetupBanner.tsx index 4d9c08be..53214309 100644 --- a/frontend/src/components/dashboard/CollapsibleSetupBanner.tsx +++ b/frontend/src/components/dashboard/CollapsibleSetupBanner.tsx @@ -125,7 +125,7 @@ export function CollapsibleSetupBanner({ remainingSections, progressPercentage, {/* Text */}

- 📋 {t('dashboard:setup_banner.title', '{{count}} paso(s) más para desbloquear todas las funciones', { count: remainingSections.length })} + 📋 {t('dashboard:setup_banner.title', '{count} paso(s) más para desbloquear todas las funciones', { count: remainingSections.length })}

{remainingSections.map(s => s.title).join(', ')} {t('dashboard:setup_banner.recommended', '(recomendado)')} diff --git a/frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx b/frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx index bb333348..c2f13d5d 100644 --- a/frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx +++ b/frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx @@ -344,6 +344,17 @@ const OnboardingWizardContent: React.FC = () => { return; } + // CRITICAL: Guard against undefined currentStep + if (!currentStep) { + console.error('❌ Current step is undefined!', { + currentStepIndex, + visibleStepsLength: VISIBLE_STEPS.length, + visibleSteps: VISIBLE_STEPS.map(s => s.id), + wizardState: wizardContext.state, + }); + return; + } + if (markStepCompleted.isPending) { console.warn(`⚠️ Step completion already in progress for "${currentStep.id}", skipping duplicate call`); return; @@ -449,7 +460,7 @@ const OnboardingWizardContent: React.FC = () => { if (response.failed_count > 0) { console.warn('⚠️ Some child tenants failed to create:', response.failed_tenants); - + // Show specific errors for each failed tenant response.failed_tenants.forEach(failed => { console.error(`Failed to create tenant ${failed.name} (${failed.location_code}):`, failed.error); @@ -492,8 +503,32 @@ const OnboardingWizardContent: React.FC = () => { } if (currentStep.id === 'completion') { - wizardContext.resetWizard(); - navigate(isNewTenant ? '/app/dashboard' : '/app'); + // CRITICAL: Call backend to mark onboarding as fully completed + if (data?.markFullyCompleted) { + console.log('🎯 [Completion] Marking onboarding as fully completed in backend...'); + try { + const { onboardingService } = await import('../../../api'); + await onboardingService.completeOnboarding(); + console.log('✅ [Completion] Onboarding marked as fully completed successfully'); + } catch (error: any) { + console.error('❌ [Completion] Failed to mark onboarding as fully completed:', error); + // If completion fails due to conditional steps, it's okay to continue + // The backend error is logged but we don't block the user + if (error?.message?.includes('incomplete steps')) { + console.warn('⚠️ [Completion] Some conditional steps incomplete - this is expected for certain tiers'); + } + } + } + + // DON'T reset wizard context here - it breaks VISIBLE_STEPS calculation + // and causes "can't access property 'component', i is undefined" error + // The context will be reset when user returns to onboarding flow + + // Don't navigate here - let the CompletionStep handle navigation + // This prevents double navigation which can cause issues + if (!data?.redirectTo) { + navigate(isNewTenant ? '/app/dashboard' : '/app'); + } } else { if (currentStepIndex < VISIBLE_STEPS.length - 1) { setCurrentStepIndex(currentStepIndex + 1); @@ -583,69 +618,141 @@ const OnboardingWizardContent: React.FC = () => { ); } + // CRITICAL: Guard against undefined currentStep in render + if (!currentStep) { + console.error('❌ Cannot render: currentStep is undefined!', { + currentStepIndex, + visibleStepsLength: VISIBLE_STEPS.length, + visibleSteps: VISIBLE_STEPS.map(s => s.id), + wizardState: wizardContext.state, + }); + return ( +

+ + +
+
+ + + +
+
+

+ {t('onboarding:errors.step_error', 'Error de configuración')} +

+

+ {t('onboarding:errors.step_missing', 'No se pudo cargar el paso actual. Por favor, recarga la página.')} +

+ +
+
+
+
+
+ ); + } + const StepComponent = currentStep.component; const progressPercentage = isNewTenant ? ((currentStepIndex + 1) / VISIBLE_STEPS.length) * 100 : userProgress?.completion_percentage || ((currentStepIndex + 1) / VISIBLE_STEPS.length) * 100; return ( -
+
{/* Progress Header */} - -
+ +
-

+

{isNewTenant ? t('onboarding:wizard.title_new', 'Nueva Panadería') : t('onboarding:wizard.title', 'Configuración Inicial')}

-

+

{t('onboarding:wizard.subtitle', 'Configura tu sistema paso a paso')}

-
-
+
+
{t('onboarding:wizard.progress.step_of', 'Paso {current} de {total}', { current: currentStepIndex + 1, total: VISIBLE_STEPS.length })}
-
+
{Math.round(progressPercentage)}% {t('onboarding:wizard.progress.completed', 'completado')}
- {/* Progress Bar */} -
-
+ {/* Enhanced Progress Bar with milestone markers */} +
+
+
+ {/* Animated shimmer effect */} +
+
+
+ + {/* Step milestone indicators */} +
+ {VISIBLE_STEPS.map((_, index) => { + const stepProgress = ((index + 1) / VISIBLE_STEPS.length) * 100; + const isCompleted = stepProgress <= progressPercentage; + const isCurrent = index === currentStepIndex; + + return ( +
+ ); + })} +
{/* Step Content */} - - -
-
-
- {currentStepIndex + 1} + + +
+
+
+
+ {currentStepIndex + 1} +
+ {/* Decorative ring */} +
-
-

+
+

{currentStep.title}

-

+

{currentStep.description}

- + {}} + onNext={() => { }} onPrevious={handleGoToPrevious} onComplete={handleStepComplete} onUpdate={handleStepUpdate} @@ -656,15 +763,15 @@ const OnboardingWizardContent: React.FC = () => { // Pass AI data and file to InventoryReviewStep currentStep.id === 'inventory-review' ? { - uploadedFile: wizardContext.state.uploadedFile, - validationResult: wizardContext.state.uploadedFileValidation, - aiSuggestions: wizardContext.state.aiSuggestions, - uploadedFileName: wizardContext.state.uploadedFileName || '', - uploadedFileSize: wizardContext.state.uploadedFileSize || 0, - } + uploadedFile: wizardContext.state.uploadedFile, + validationResult: wizardContext.state.uploadedFileValidation, + aiSuggestions: wizardContext.state.aiSuggestions, + uploadedFileName: wizardContext.state.uploadedFileName || '', + uploadedFileSize: wizardContext.state.uploadedFileSize || 0, + } : // Pass inventory items to InitialStockEntryStep currentStep.id === 'initial-stock-entry' && wizardContext.state.inventoryItems - ? { + ? { productsWithStock: wizardContext.state.inventoryItems.map(item => ({ id: item.id, name: item.name, @@ -674,7 +781,7 @@ const OnboardingWizardContent: React.FC = () => { initialStock: undefined, })) } - : undefined + : undefined } /> diff --git a/frontend/src/components/domain/onboarding/steps/BakeryTypeSelectionStep.tsx b/frontend/src/components/domain/onboarding/steps/BakeryTypeSelectionStep.tsx index e4e2e85e..010bdcbd 100644 --- a/frontend/src/components/domain/onboarding/steps/BakeryTypeSelectionStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/BakeryTypeSelectionStep.tsx @@ -118,13 +118,16 @@ export const BakeryTypeSelectionStep: React.FC = ( }; return ( -
+
{/* Header */} -
-

+
+
+
🏪
+
+

{t('onboarding:bakery_type.title', '¿Qué tipo de panadería tienes?')}

-

+

{t( 'onboarding:bakery_type.subtitle', 'Esto nos ayudará a personalizar la experiencia y mostrarte solo las funciones que necesitas' @@ -133,8 +136,8 @@ export const BakeryTypeSelectionStep: React.FC = (

{/* Bakery Type Cards */} -
- {bakeryTypes.map((type) => { +
+ {bakeryTypes.map((type, index) => { const isSelected = selectedType === type.id; const isHovered = hoveredType === type.id; @@ -145,70 +148,79 @@ export const BakeryTypeSelectionStep: React.FC = ( onClick={() => handleSelectType(type.id)} onMouseEnter={() => setHoveredType(type.id)} onMouseLeave={() => setHoveredType(null)} + style={{ animationDelay: `${index * 100}ms` }} className={` relative cursor-pointer transition-all duration-300 overflow-hidden - border-2 rounded-lg text-left w-full + border-2 rounded-2xl text-left w-full group bg-[var(--bg-secondary)] + transform-gpu ${isSelected - ? 'border-[var(--color-primary)] shadow-lg ring-2 ring-[var(--color-primary)]/50 scale-[1.02]' - : 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:shadow-md' + ? 'border-[var(--color-primary)] shadow-2xl ring-4 ring-[var(--color-primary)]/30 scale-[1.03] -translate-y-1' + : 'border-[var(--border-color)] hover:border-[var(--color-primary)]/60 hover:shadow-xl hover:scale-[1.02] hover:-translate-y-0.5' } - ${isHovered && !isSelected ? 'shadow-sm' : ''} + ${isHovered && !isSelected ? 'shadow-lg' : ''} `} > {/* Selection Indicator */} {isSelected && ( -
-
- +
+
+
)} - {/* Accent Background */} -
+ {/* Accent Background with gradient */} +
+ + {/* Hover shimmer effect */} +
{/* Content */} -
+
{/* Icon & Title */} -
-
{type.icon}
-

+
+
+ {type.icon} +
+

{type.name}

-

+

{type.description}

{/* Features */} -
-

+
+

+ {t('onboarding:bakery_type.features_label', 'Características')}

-
    - {type.features.map((feature, index) => ( +
      + {type.features.map((feature, featureIndex) => (
    • - - {feature} + + {feature}
    • ))}
{/* Examples */} -
-

+
+

+ {t('onboarding:bakery_type.examples_label', 'Ejemplos')}

- {type.examples.map((example, index) => ( + {type.examples.map((example, exampleIndex) => ( {example} @@ -223,16 +235,17 @@ export const BakeryTypeSelectionStep: React.FC = ( {/* Additional Info */} {selectedType && ( -
-
-
+
+
+
{bakeryTypes.find(t => t.id === selectedType)?.icon}
-
-

+
+

+ {t('onboarding:bakery_type.selected_info_title', 'Perfecto para tu panadería')}

-

+

{selectedType === 'production' && t( 'onboarding:bakery_type.production.selected_info', @@ -255,22 +268,27 @@ export const BakeryTypeSelectionStep: React.FC = ( )} {/* Help Text & Continue Button */} -

-

- {t( - 'onboarding:bakery_type.help_text', - '💡 No te preocupes, siempre puedes cambiar esto más tarde en la configuración' - )} -

+
+
+ 💡 +

+ {t( + 'onboarding:bakery_type.help_text', + 'No te preocupes, siempre puedes cambiar esto más tarde en la configuración' + )} +

+
diff --git a/frontend/src/components/domain/onboarding/steps/CompletionStep.tsx b/frontend/src/components/domain/onboarding/steps/CompletionStep.tsx index 9e5d2d8c..c9eb1af7 100644 --- a/frontend/src/components/domain/onboarding/steps/CompletionStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/CompletionStep.tsx @@ -20,54 +20,59 @@ export const CompletionStep: React.FC = ({ const navigate = useNavigate(); const currentTenant = useCurrentTenant(); - const handleStartUsingSystem = async () => { - // CRITICAL: Ensure tenant access is loaded before navigating - console.log('🔄 [CompletionStep] Ensuring tenant setup is complete before dashboard navigation...'); - - // Small delay to ensure any pending state updates complete - await new Promise(resolve => setTimeout(resolve, 500)); - - onComplete({ redirectTo: '/app/dashboard' }); - navigate('/app/dashboard'); - }; - const handleExploreDashboard = async () => { // CRITICAL: Ensure tenant access is loaded before navigating console.log('🔄 [CompletionStep] Ensuring tenant setup is complete before dashboard navigation...'); - // Small delay to ensure any pending state updates complete - await new Promise(resolve => setTimeout(resolve, 500)); + // Mark onboarding as fully completed before navigating + // This ensures the dashboard doesn't redirect back to onboarding + await onComplete({ + redirectTo: '/app/dashboard', + markFullyCompleted: true + }); - onComplete({ redirectTo: '/app/dashboard' }); + // Small delay to ensure backend state updates complete + await new Promise(resolve => setTimeout(resolve, 800)); + + console.log('✅ [CompletionStep] Navigating to dashboard...'); navigate('/app/dashboard'); }; return ( -
+
+ {/* Confetti effect */} +
+
🎉
+
+
🎊
+
+ {/* Animated Success Icon */} -
-
-
- +
+
+
+
+
{/* Success Message */} -
-

+
+

{t('onboarding:completion.congratulations', '¡Felicidades! Tu Sistema Está Listo')}

-

+

{t('onboarding:completion.all_configured', 'Has configurado exitosamente {{name}} con nuestro sistema de gestión inteligente. Todo está listo para empezar a optimizar tu panadería.', { name: currentTenant?.name })}

{/* What You Configured */} -
-

+
+

+ 📋 {t('onboarding:completion.what_configured', 'Lo Que Has Configurado')}

-
+
@@ -155,68 +160,68 @@ export const CompletionStep: React.FC = ({
{/* Quick Access Cards */} -
+
{/* Tips for Success */} -
-
-
- +
+
+
+
-

+

{t('onboarding:completion.tips_title', 'Consejos para Maximizar tu Éxito')}

@@ -250,13 +255,13 @@ export const CompletionStep: React.FC = ({
{/* Primary Action Button */} -
+
diff --git a/frontend/src/components/domain/onboarding/steps/InitialStockEntryStep.tsx b/frontend/src/components/domain/onboarding/steps/InitialStockEntryStep.tsx index 15c662d7..b57fe1d3 100644 --- a/frontend/src/components/domain/onboarding/steps/InitialStockEntryStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/InitialStockEntryStep.tsx @@ -1,11 +1,11 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Package, Salad, AlertCircle, ArrowRight, ArrowLeft, CheckCircle } from 'lucide-react'; import Button from '../../../ui/Button/Button'; import Card from '../../../ui/Card/Card'; import Input from '../../../ui/Input/Input'; import { useCurrentTenant } from '../../../../stores/tenant.store'; -import { useAddStock } from '../../../../api/hooks/inventory'; +import { useAddStock, useStock } from '../../../../api/hooks/inventory'; import InfoCard from '../../../ui/InfoCard'; export interface ProductWithStock { @@ -38,6 +38,7 @@ export const InitialStockEntryStep: React.FC = ({ const currentTenant = useCurrentTenant(); const tenantId = currentTenant?.id || ''; const addStockMutation = useAddStock(); + const { data: stockData } = useStock(tenantId); const [isSaving, setIsSaving] = useState(false); const [products, setProducts] = useState(() => { @@ -54,6 +55,30 @@ export const InitialStockEntryStep: React.FC = ({ })); }); + // Merge existing stock from backend on mount + useEffect(() => { + if (stockData?.items && products.length > 0) { + console.log('🔄 Merging backend stock data into initial stock entry state...', { itemsCount: stockData.items.length }); + + let hasChanges = false; + const updatedProducts = products.map(p => { + const existingStock = stockData?.items?.find(s => s.ingredient_id === p.id); + if (existingStock && p.initialStock !== existingStock.current_quantity) { + hasChanges = true; + return { + ...p, + initialStock: existingStock.current_quantity + }; + } + return p; + }); + + if (hasChanges) { + setProducts(updatedProducts); + } + } + }, [stockData, products]); // Run when stock data changes or products list is initialized + const ingredients = products.filter(p => p.type === 'ingredient'); const finishedProducts = products.filter(p => p.type === 'finished_product'); @@ -87,30 +112,51 @@ export const InitialStockEntryStep: React.FC = ({ const handleContinue = async () => { setIsSaving(true); try { - // Create stock entries for products with initial stock > 0 - const stockEntries = products.filter(p => p.initialStock && p.initialStock > 0); + // STEP 0: Check for existing stock to avoid duplication + const existingStockMap = new Map( + stockData?.items?.map(s => [s.ingredient_id, s.current_quantity]) || [] + ); - if (stockEntries.length > 0) { - // Create stock entries in parallel - const stockPromises = stockEntries.map(product => + // Create stock entries only for products where: + // 1. initialStock is defined AND > 0 + // 2. AND (it doesn't exist OR the value is different) + const stockEntriesToSync = products.filter(p => { + const currentVal = p.initialStock ?? 0; + const backendVal = existingStockMap.get(p.id); + + // Only sync if it's new (> 0 and doesn't exist) or changed + if (backendVal === undefined) { + return currentVal > 0; + } + return currentVal !== backendVal; + }); + + console.log(`📦 Stock processing: ${stockEntriesToSync.length} to sync, ${products.length - stockEntriesToSync.length} skipped.`); + + if (stockEntriesToSync.length > 0) { + // Create or update stock entries + // Note: useAddStock currently handles creation/initial set. + // If the backend requires a different endpoint for updates, this might need adjustment. + // For onboarding, we assume addStock is a "set-and-forget" for initial levels. + const stockPromises = stockEntriesToSync.map(product => addStockMutation.mutateAsync({ tenantId, stockData: { ingredient_id: product.id, - current_quantity: product.initialStock!, // The actual stock quantity - unit_cost: 0, // Default cost, can be updated later + current_quantity: product.initialStock || 0, + unit_cost: 0, } }) ); await Promise.all(stockPromises); - console.log(`✅ Created ${stockEntries.length} stock entries successfully`); + console.log(`✅ Synced ${stockEntriesToSync.length} stock entries successfully`); } onComplete?.(); } catch (error) { - console.error('Error creating stock entries:', error); - alert(t('onboarding:stock.error_creating_stock', 'Error al crear los niveles de stock. Por favor, inténtalo de nuevo.')); + console.error('Error syncing stock entries:', error); + alert(t('onboarding:stock.error_creating_stock', 'Error al guardar los niveles de stock. Por favor, inténtalo de nuevo.')); } finally { setIsSaving(false); } diff --git a/frontend/src/components/domain/onboarding/steps/InventoryReviewStep.tsx b/frontend/src/components/domain/onboarding/steps/InventoryReviewStep.tsx index 85feaf30..00664ee3 100644 --- a/frontend/src/components/domain/onboarding/steps/InventoryReviewStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/InventoryReviewStep.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '../../../ui/Button'; import { useCurrentTenant } from '../../../../stores/tenant.store'; -import { useCreateIngredient } from '../../../../api/hooks/inventory'; +import { useCreateIngredient, useIngredients } from '../../../../api/hooks/inventory'; import { useImportSalesData } from '../../../../api/hooks/sales'; import type { ProductSuggestionResponse, IngredientCreate } from '../../../../api/types/inventory'; import { ProductType, UnitOfMeasure, IngredientCategory, ProductCategory } from '../../../../api/types/inventory'; @@ -14,6 +14,7 @@ interface InventoryReviewStepProps { onComplete: (data: { inventoryItemsCreated: number; salesDataImported: boolean; + inventoryItems?: any[]; }) => void; isFirstStep: boolean; isLastStep: boolean; @@ -140,11 +141,15 @@ export const InventoryReviewStep: React.FC = ({ // API hooks const createIngredientMutation = useCreateIngredient(); const importSalesMutation = useImportSalesData(); + const { data: existingIngredients } = useIngredients(tenantId); - // Initialize with AI suggestions + // Initialize with AI suggestions AND existing ingredients useEffect(() => { - if (initialData?.aiSuggestions) { - const items: InventoryItemForm[] = initialData.aiSuggestions.map((suggestion, index) => ({ + // 1. Start with AI suggestions if available + let items: InventoryItemForm[] = []; + + if (initialData?.aiSuggestions && initialData.aiSuggestions.length > 0) { + items = initialData.aiSuggestions.map((suggestion, index) => ({ id: `ai-${index}-${Date.now()}`, name: suggestion.suggested_name, product_type: suggestion.product_type as ProductType, @@ -157,9 +162,43 @@ export const InventoryReviewStep: React.FC = ({ average_daily_sales: suggestion.sales_data.average_daily_sales, } : undefined, })); + } + + // 2. Merge/Override with existing backend ingredients + if (existingIngredients && existingIngredients.length > 0) { + existingIngredients.forEach(ing => { + // Check if we already have this by name (from AI) + const existingIdx = items.findIndex(item => + item.name.toLowerCase() === ing.name.toLowerCase() && + item.product_type === ing.product_type + ); + + if (existingIdx !== -1) { + // Update existing item with real ID + items[existingIdx] = { + ...items[existingIdx], + id: ing.id, + category: ing.category || items[existingIdx].category, + unit_of_measure: ing.unit_of_measure as UnitOfMeasure, + }; + } else { + // Add as new item (this handles items created in previous sessions/attempts) + items.push({ + id: ing.id, + name: ing.name, + product_type: ing.product_type, + category: ing.category || '', + unit_of_measure: ing.unit_of_measure as UnitOfMeasure, + isSuggested: false, + }); + } + }); + } + + if (items.length > 0) { setInventoryItems(items); } - }, [initialData]); + }, [initialData, existingIngredients]); // Filter items const filteredItems = inventoryItems.filter(item => { @@ -277,43 +316,45 @@ export const InventoryReviewStep: React.FC = ({ setFormErrors({}); try { - // STEP 1: Create all inventory items in parallel - // This MUST happen BEFORE sales import because sales records reference inventory IDs - console.log('📦 Creating inventory items...', inventoryItems.length); - console.log('📋 Items to create:', inventoryItems.map(item => ({ - name: item.name, - product_type: item.product_type, - category: item.category, - unit_of_measure: item.unit_of_measure - }))); + // STEP 0: Check for existing ingredients to avoid duplication + const existingNamesAndTypes = new Set( + existingIngredients?.map(i => `${i.name.toLowerCase()}-${i.product_type}`) || [] + ); - const createPromises = inventoryItems.map((item, index) => { + const itemsToCreate = inventoryItems.filter(item => { + const key = `${item.name.toLowerCase()}-${item.product_type}`; + return !existingNamesAndTypes.has(key); + }); + + const existingMatches = existingIngredients?.filter(i => { + const key = `${i.name.toLowerCase()}-${i.product_type}`; + return inventoryItems.some(item => `${item.name.toLowerCase()}-${item.product_type}` === key); + }) || []; + + console.log(`📦 Inventory processing: ${itemsToCreate.length} to create, ${existingMatches.length} already exist.`); + + // STEP 1: Create new inventory items in parallel + const createPromises = itemsToCreate.map((item, index) => { const ingredientData: IngredientCreate = { name: item.name, product_type: item.product_type, category: item.category, unit_of_measure: item.unit_of_measure as UnitOfMeasure, - // All other fields are optional now! }; - console.log(`🔄 Creating ingredient ${index + 1}/${inventoryItems.length}:`, ingredientData); - return createIngredientMutation.mutateAsync({ tenantId, ingredientData, }).catch(error => { console.error(`❌ Failed to create ingredient "${item.name}":`, error); - console.error('Failed ingredient data:', ingredientData); throw error; }); }); - const createdIngredients = await Promise.all(createPromises); - console.log('✅ Inventory items created successfully'); - console.log('📋 Created ingredient IDs:', createdIngredients.map(ing => ({ name: ing.name, id: ing.id }))); + const newlyCreatedIngredients = await Promise.all(createPromises); + console.log('✅ New inventory items created successfully'); // STEP 2: Import sales data (only if file was uploaded) - // Now that inventory exists, sales records can reference the inventory IDs let salesImported = false; if (initialData?.uploadedFile && tenantId) { try { @@ -325,28 +366,34 @@ export const InventoryReviewStep: React.FC = ({ salesImported = true; console.log('✅ Sales data imported successfully'); } catch (salesError) { - console.error('⚠️ Sales import failed (non-blocking):', salesError); - // Don't block onboarding if sales import fails - // Inventory is already created, which is the critical part + console.error('⚠️ Sales import failed (non-blocking):', salesError); } } - // Complete the step with metadata and inventory items - // Map created ingredients to include their real UUIDs - const itemsWithRealIds = createdIngredients.map(ingredient => ({ - id: ingredient.id, // Real UUID from the API - name: ingredient.name, - product_type: ingredient.product_type, - category: ingredient.category, - unit_of_measure: ingredient.unit_of_measure, - })); + // STEP 3: Consolidate all items (existing + newly created) + const allItemsWithRealIds = [ + ...existingMatches.map(i => ({ + id: i.id, + name: i.name, + product_type: i.product_type, + category: i.category, + unit_of_measure: i.unit_of_measure, + })), + ...newlyCreatedIngredients.map(i => ({ + id: i.id, + name: i.name, + product_type: i.product_type, + category: i.category, + unit_of_measure: i.unit_of_measure, + })) + ]; - console.log('📦 Passing items with real IDs to next step:', itemsWithRealIds); + console.log('📦 Passing items with real IDs to next step:', allItemsWithRealIds); onComplete({ - inventoryItemsCreated: createdIngredients.length, + inventoryItemsCreated: newlyCreatedIngredients.length, salesDataImported: salesImported, - inventoryItems: itemsWithRealIds, // Pass the created items with real UUIDs to the next step + inventoryItems: allItemsWithRealIds, }); } catch (error) { console.error('Error creating inventory items:', error); @@ -439,21 +486,19 @@ export const InventoryReviewStep: React.FC = ({
diff --git a/frontend/src/components/domain/onboarding/steps/RegisterTenantStep.tsx b/frontend/src/components/domain/onboarding/steps/RegisterTenantStep.tsx index 4c666096..de2079c1 100644 --- a/frontend/src/components/domain/onboarding/steps/RegisterTenantStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/RegisterTenantStep.tsx @@ -1,8 +1,8 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Button, Input } from '../../../ui'; import { AddressAutocomplete } from '../../../ui/AddressAutocomplete'; -import { useRegisterBakery } from '../../../../api/hooks/tenant'; -import { BakeryRegistration } from '../../../../api/types/tenant'; +import { useRegisterBakery, useTenant, useUpdateTenant } from '../../../../api/hooks/tenant'; +import { BakeryRegistration, TenantUpdate } from '../../../../api/types/tenant'; import { AddressResult } from '../../../../services/api/geocodingApi'; import { useWizardContext } from '../context'; import { poiContextApi } from '../../../../services/api/poiContextApi'; @@ -34,6 +34,7 @@ export const RegisterTenantStep: React.FC = ({ isFirstStep }) => { const wizardContext = useWizardContext(); + const tenantId = wizardContext.state.tenantId; // Check if user is enterprise tier for conditional labels const subscriptionTier = localStorage.getItem('subscription_tier'); @@ -52,8 +53,31 @@ export const RegisterTenantStep: React.FC = ({ business_model: businessModel }); + // Fetch existing tenant data if we have a tenantId (persistence) + const { data: existingTenant, isLoading: isLoadingTenant } = useTenant(tenantId || ''); + + // Update formData when existing tenant data is fetched + useEffect(() => { + if (existingTenant) { + console.log('🔄 Populating RegisterTenantStep with existing data:', existingTenant); + setFormData({ + name: existingTenant.name, + address: existingTenant.address, + postal_code: existingTenant.postal_code, + phone: existingTenant.phone || '', + city: existingTenant.city, + business_type: existingTenant.business_type, + business_model: existingTenant.business_model || businessModel + }); + + // Update location in context if available from tenant + // Note: Backend might not store lat/lon directly in Tenant table in all versions, + // but if we had them or if we want to re-trigger geocoding, we'd handle it here. + } + }, [existingTenant, businessModel]); + // Update business_model when bakeryType changes in context - React.useEffect(() => { + useEffect(() => { const newBusinessModel = getBakeryBusinessModel(wizardContext.state.bakeryType); if (newBusinessModel !== formData.business_model) { setFormData(prev => ({ @@ -65,6 +89,7 @@ export const RegisterTenantStep: React.FC = ({ const [errors, setErrors] = useState>({}); const registerBakery = useRegisterBakery(); + const updateTenant = useUpdateTenant(); const handleInputChange = (field: keyof BakeryRegistration, value: string) => { setFormData(prev => ({ @@ -143,14 +168,31 @@ export const RegisterTenantStep: React.FC = ({ return; } - console.log('📝 Registering tenant with data:', { + console.log('📝 Submitting tenant data:', { + isUpdate: !!tenantId, bakeryType: wizardContext.state.bakeryType, business_model: formData.business_model, formData }); try { - const tenant = await registerBakery.mutateAsync(formData); + let tenant; + if (tenantId) { + // Update existing tenant + const updateData: TenantUpdate = { + name: formData.name, + address: formData.address, + phone: formData.phone, + business_type: formData.business_type, + business_model: formData.business_model + }; + tenant = await updateTenant.mutateAsync({ tenantId, updateData }); + console.log('✅ Tenant updated successfully:', tenant.id); + } else { + // Create new tenant + tenant = await registerBakery.mutateAsync(formData); + console.log('✅ Tenant registered successfully:', tenant.id); + } // Trigger POI detection in the background (non-blocking) // This replaces the removed POI Detection step @@ -203,29 +245,51 @@ export const RegisterTenantStep: React.FC = ({ }; return ( -
-
- handleInputChange('name', e.target.value)} - error={errors.name} - isRequired - /> +
+ {/* Informational header */} +
+
+
{isEnterprise ? '🏭' : '🏪'}
+
+

+ {isEnterprise ? 'Registra tu Obrador Central' : 'Registra tu Panadería'} +

+

+ {isEnterprise + ? 'Ingresa los datos de tu obrador principal. Después podrás agregar las sucursales.' + : 'Completa la información básica de tu panadería para comenzar.'} +

+
+
+
- handleInputChange('phone', e.target.value)} - error={errors.phone} - isRequired - /> +
+
+ handleInputChange('name', e.target.value)} + error={errors.name} + isRequired + /> +
-
-
@@ -1007,16 +1007,29 @@ export const InventorySetupStep: React.FC = ({ onUpdate, onCompl
)} - {/* Continue button - only shown when used in onboarding context */} - {onComplete && ( -
- + {/* Navigation buttons */} + {!isAdding && onComplete && ( +
+
+ +
+ +
+ +
)}
diff --git a/frontend/src/components/domain/setup-wizard/steps/QualitySetupStep.tsx b/frontend/src/components/domain/setup-wizard/steps/QualitySetupStep.tsx index cee1cc17..ab8d3c36 100644 --- a/frontend/src/components/domain/setup-wizard/steps/QualitySetupStep.tsx +++ b/frontend/src/components/domain/setup-wizard/steps/QualitySetupStep.tsx @@ -1,13 +1,12 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import type { SetupStepProps } from '../SetupWizard'; +import { SetupStepProps } from '../types'; import { useQualityTemplates, useCreateQualityTemplate } from '../../../../api/hooks/qualityTemplates'; import { useCurrentTenant } from '../../../../stores/tenant.store'; import { useAuthUser } from '../../../../stores/auth.store'; -import { QualityCheckType, ProcessStage } from '../../../../api/types/qualityTemplates'; -import type { QualityCheckTemplateCreate } from '../../../../api/types/qualityTemplates'; +import { QualityCheckType, ProcessStage, QualityCheckTemplate, QualityCheckTemplateCreate } from '../../../../api/types/qualityTemplates'; -export const QualitySetupStep: React.FC = ({ onUpdate, onComplete, canContinue }) => { +export const QualitySetupStep: React.FC = ({ onUpdate, onComplete, onPrevious, canContinue }) => { const { t } = useTranslation(); // Get tenant ID and user @@ -75,14 +74,14 @@ export const QualitySetupStep: React.FC = ({ onUpdate, onComplet try { const templateData: QualityCheckTemplateCreate = { name: formData.name, - check_type: formData.check_type, + check_type: (formData.check_type as QualityCheckType) || QualityCheckType.VISUAL, description: formData.description || undefined, applicable_stages: formData.applicable_stages, is_required: formData.is_required, is_critical: formData.is_critical, is_active: true, weight: formData.is_critical ? 10 : 5, - created_by: userId, + created_by: userId || '', }; await createTemplateMutation.mutateAsync(templateData); @@ -165,7 +164,7 @@ export const QualitySetupStep: React.FC = ({ onUpdate, onComplet - {t('setup_wizard:quality.added_count', { count: templates.length, defaultValue: '{{count}} quality check added' })} + {t('setup_wizard:quality.added_count', { count: templates.length, defaultValue: '{count} quality check added' })}
{templates.length >= 2 ? ( @@ -252,7 +251,7 @@ export const QualitySetupStep: React.FC = ({ onUpdate, onComplet type="text" value={formData.name} onChange={(e) => setFormData({ ...formData, name: e.target.value })} - className={`w-full px-3 py-2 bg-[var(--bg-primary)] border ${errors.name ? 'border-[var(--color-error)]' : 'border-[var(--border-secondary)]'} rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]`} + className={`w - full px - 3 py - 2 bg - [var(--bg - primary)] border ${errors.name ? 'border-[var(--color-error)]' : 'border-[var(--border-secondary)]'} rounded - lg focus: outline - none focus: ring - 2 focus: ring - [var(--color - primary)]text - [var(--text - primary)]`} placeholder={t('setup_wizard:quality.placeholders.name', 'e.g., Crust color check, Dough temperature')} /> {errors.name &&

{errors.name}

} @@ -274,11 +273,10 @@ export const QualitySetupStep: React.FC = ({ onUpdate, onComplet console.log('Check type clicked:', option.value, 'current:', formData.check_type); setFormData(prev => ({ ...prev, check_type: option.value })); }} - className={`p-3 text-left border-2 rounded-lg transition-all cursor-pointer ${ - formData.check_type === option.value - ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/20 shadow-lg ring-2 ring-[var(--color-primary)]/30' - : 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]' - }`} + className={`p - 3 text - left border - 2 rounded - lg transition - all cursor - pointer ${formData.check_type === option.value + ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/20 shadow-lg ring-2 ring-[var(--color-primary)]/30' + : 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]' + } `} >
{option.icon}
{option.label}
@@ -325,11 +323,10 @@ export const QualitySetupStep: React.FC = ({ onUpdate, onComplet : [...prev.applicable_stages, option.value] })); }} - className={`p-2 text-sm text-left border-2 rounded-lg transition-all cursor-pointer ${ - formData.applicable_stages.includes(option.value) - ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/20 text-[var(--color-primary)] font-semibold shadow-md ring-1 ring-[var(--color-primary)]/30' - : 'border-[var(--border-secondary)] text-[var(--text-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]' - }`} + className={`p - 2 text - sm text - left border - 2 rounded - lg transition - all cursor - pointer ${formData.applicable_stages.includes(option.value) + ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/20 text-[var(--color-primary)] font-semibold shadow-md ring-1 ring-[var(--color-primary)]/30' + : 'border-[var(--border-secondary)] text-[var(--text-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]' + } `} > {option.label} @@ -429,16 +426,29 @@ export const QualitySetupStep: React.FC = ({ onUpdate, onComplet
)} - {/* Continue button - only shown when used in onboarding context */} - {onComplete && ( -
- + {/* Navigation buttons */} + {!isAdding && onComplete && ( +
+
+ +
+ +
+ +
)}
diff --git a/frontend/src/components/domain/setup-wizard/steps/RecipesSetupStep.tsx b/frontend/src/components/domain/setup-wizard/steps/RecipesSetupStep.tsx index 14ca442e..df94cd61 100644 --- a/frontend/src/components/domain/setup-wizard/steps/RecipesSetupStep.tsx +++ b/frontend/src/components/domain/setup-wizard/steps/RecipesSetupStep.tsx @@ -1,13 +1,12 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import type { SetupStepProps } from '../SetupWizard'; +import { SetupStepProps } from '../types'; import { useRecipes, useCreateRecipe, useUpdateRecipe, useDeleteRecipe } from '../../../../api/hooks/recipes'; import { useIngredients } from '../../../../api/hooks/inventory'; import { useCurrentTenant } from '../../../../stores/tenant.store'; import { useAuthUser } from '../../../../stores/auth.store'; -import { MeasurementUnit } from '../../../../api/types/recipes'; +import { RecipeStatus, MeasurementUnit, type RecipeCreate, type RecipeIngredientCreate, type RecipeResponse } from '../../../../api/types/recipes'; import { ProductType } from '../../../../api/types/inventory'; -import type { RecipeCreate, RecipeIngredientCreate } from '../../../../api/types/recipes'; import { getAllRecipeTemplates, matchIngredientToTemplate, type RecipeTemplate } from '../data/recipeTemplates'; import { QuickAddIngredientModal } from '../../inventory/QuickAddIngredientModal'; @@ -18,7 +17,7 @@ interface RecipeIngredientForm { ingredient_order: number; } -export const RecipesSetupStep: React.FC = ({ onUpdate, onComplete, canContinue }) => { +export const RecipesSetupStep: React.FC = ({ onUpdate, onComplete, onPrevious, canContinue }) => { const { t } = useTranslation(); // Get tenant ID @@ -63,7 +62,7 @@ export const RecipesSetupStep: React.FC = ({ onUpdate, onComplet useEffect(() => { const count = recipes.length; onUpdate?.({ - itemsCount: count, + itemCount: count, canContinue: count >= 1, }); }, [recipes.length, onUpdate]); @@ -384,7 +383,7 @@ export const RecipesSetupStep: React.FC = ({ onUpdate, onComplet
{recipes.length >= 1 && ( @@ -606,7 +605,7 @@ export const RecipesSetupStep: React.FC = ({ onUpdate, onComplet id="yield-unit" value={formData.yield_unit} onChange={(e) => setFormData({ ...formData, yield_unit: e.target.value as MeasurementUnit })} - className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" + className={`w-full px-3 py-2 bg-[var(--bg-primary)] border ${errors.yield_unit ? 'border-[var(--color-error)]' : 'border-[var(--border-secondary)]'} rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]`} > {unitOptions.map((option) => ( ))} + {errors.yield_unit &&

{errors.yield_unit}

}
@@ -765,23 +765,34 @@ export const RecipesSetupStep: React.FC = ({ onUpdate, onComplet
)} - {/* Navigation - Show Next button when minimum requirement met */} - {recipes.length >= 1 && !isAdding && ( -
-
- - - - - {t('setup_wizard:recipes.minimum_met', '{{count}} recipe(s) added - Ready to continue!', { count: recipes.length })} - + {/* Navigation buttons */} + {!isAdding && ( +
+
+ +
+ +
+ {!canContinue && recipes.length === 0 && ( +

+ {t('setup_wizard:recipes.add_minimum', 'Agrega al menos 1 receta para continuar')} +

+ )} +
-
)} diff --git a/frontend/src/components/domain/setup-wizard/steps/ReviewSetupStep.tsx b/frontend/src/components/domain/setup-wizard/steps/ReviewSetupStep.tsx index 5234f7f7..12bcd484 100644 --- a/frontend/src/components/domain/setup-wizard/steps/ReviewSetupStep.tsx +++ b/frontend/src/components/domain/setup-wizard/steps/ReviewSetupStep.tsx @@ -1,14 +1,16 @@ import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import type { SetupStepProps } from '../SetupWizard'; +import { SetupStepProps } from '../types'; import { useSuppliers } from '../../../../api/hooks/suppliers'; import { useIngredients } from '../../../../api/hooks/inventory'; import { useRecipes } from '../../../../api/hooks/recipes'; import { useQualityTemplates } from '../../../../api/hooks/qualityTemplates'; import { useCurrentTenant } from '../../../../stores/tenant.store'; import { useAuthUser } from '../../../../stores/auth.store'; +import { SupplierStatus } from '../../../../api/types/suppliers'; +import { QualityCheckTemplateList } from '../../../../api/types/qualityTemplates'; -export const ReviewSetupStep: React.FC = ({ onUpdate, onComplete, canContinue }) => { +export const ReviewSetupStep: React.FC = ({ onUpdate, onComplete, onPrevious, canContinue }) => { const { t } = useTranslation(); // Get tenant ID @@ -25,7 +27,7 @@ export const ReviewSetupStep: React.FC = ({ onUpdate, onComplete const suppliers = suppliersData || []; const ingredients = ingredientsData || []; const recipes = recipesData || []; - const qualityTemplates = qualityTemplatesData || []; + const qualityTemplates = (qualityTemplatesData as unknown as QualityCheckTemplateList)?.templates || []; const isLoading = suppliersLoading || ingredientsLoading || recipesLoading || qualityLoading; @@ -146,7 +148,7 @@ export const ReviewSetupStep: React.FC = ({ onUpdate, onComplete

{supplier.email}

)}
- {supplier.is_active && ( + {supplier.status === SupplierStatus.ACTIVE && ( )}
@@ -307,16 +309,29 @@ export const ReviewSetupStep: React.FC = ({ onUpdate, onComplete )} - {/* Continue button - only shown when used in onboarding context */} + {/* Navigation buttons */} {onComplete && ( -
- +
+
+ +
+ +
+ +
)}
diff --git a/frontend/src/components/domain/setup-wizard/steps/SuppliersSetupStep.tsx b/frontend/src/components/domain/setup-wizard/steps/SuppliersSetupStep.tsx index 441e2b9f..7e5e50b4 100644 --- a/frontend/src/components/domain/setup-wizard/steps/SuppliersSetupStep.tsx +++ b/frontend/src/components/domain/setup-wizard/steps/SuppliersSetupStep.tsx @@ -1,10 +1,10 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import type { SetupStepProps } from '../SetupWizard'; +import { SetupStepProps } from '../types'; import { useSuppliers, useCreateSupplier, useUpdateSupplier, useDeleteSupplier } from '../../../../api/hooks/suppliers'; +import { SupplierType } from '../../../../api/types/suppliers'; import { useCurrentTenant } from '../../../../stores/tenant.store'; import { useAuthUser } from '../../../../stores/auth.store'; -import { SupplierType } from '../../../../api/types/suppliers'; import type { SupplierCreate, SupplierUpdate } from '../../../../api/types/suppliers'; import { SupplierProductManager } from './SupplierProductManager'; @@ -49,7 +49,7 @@ export const SuppliersSetupStep: React.FC = ({ // Notify parent when count changes useEffect(() => { onUpdate?.({ - itemsCount: suppliers.length, + itemCount: suppliers.length, canContinue: suppliers.length >= 1, }); }, [suppliers.length, onUpdate]); @@ -115,7 +115,7 @@ export const SuppliersSetupStep: React.FC = ({ const resetForm = () => { setFormData({ name: '', - supplier_type: 'ingredients', + supplier_type: SupplierType.INGREDIENTS, contact_person: '', phone: '', email: '', @@ -180,7 +180,7 @@ export const SuppliersSetupStep: React.FC = ({ - {t('setup_wizard:suppliers.added_count', { count: suppliers.length, defaultValue: '{{count}} supplier added' })} + {t('setup_wizard:suppliers.added_count', { count: suppliers.length, defaultValue: '{count} supplier added' })}
{suppliers.length >= 1 && ( @@ -441,13 +441,13 @@ export const SuppliersSetupStep: React.FC = ({ {/* Navigation buttons */}
- {!isFirstStep && ( + {onPrevious && ( )} {onSkip && suppliers.length === 0 && ( @@ -462,18 +462,18 @@ export const SuppliersSetupStep: React.FC = ({
- {!canContinue && ( + {!canContinue && suppliers.length === 0 && (

- {t('setup_wizard:suppliers.add_minimum', 'Add at least 1 supplier to continue')} + {t('setup_wizard:suppliers.add_minimum', 'Agrega al menos 1 proveedor para continuar')}

)}
diff --git a/frontend/src/components/domain/setup-wizard/steps/TeamSetupStep.tsx b/frontend/src/components/domain/setup-wizard/steps/TeamSetupStep.tsx index 4cc5fcc6..4eb39f2b 100644 --- a/frontend/src/components/domain/setup-wizard/steps/TeamSetupStep.tsx +++ b/frontend/src/components/domain/setup-wizard/steps/TeamSetupStep.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import type { SetupStepProps } from '../SetupWizard'; +import { SetupStepProps } from '../types'; interface TeamMember { id: string; @@ -9,7 +9,7 @@ interface TeamMember { role: string; } -export const TeamSetupStep: React.FC = ({ onUpdate, onComplete, canContinue }) => { +export const TeamSetupStep: React.FC = ({ onUpdate, onComplete, onPrevious, canContinue }) => { const { t } = useTranslation(); // Local state for team members (will be sent to backend when API is available) @@ -25,8 +25,8 @@ export const TeamSetupStep: React.FC = ({ onUpdate, onComplete, // Notify parent - Team step is always optional, so always canContinue useEffect(() => { onUpdate?.({ - itemsCount: teamMembers.length, - canContinue: true, // Always true since this step is optional + itemCount: teamMembers.length, + canContinue: teamMembers.length > 0, }); }, [teamMembers.length, onUpdate]); @@ -138,7 +138,7 @@ export const TeamSetupStep: React.FC = ({ onUpdate, onComplete, - {t('setup_wizard:team.added_count', { count: teamMembers.length, defaultValue: '{{count}} team member added' })} + {t('setup_wizard:team.added_count', { count: teamMembers.length, defaultValue: '{count} team member added' })}
@@ -247,11 +247,10 @@ export const TeamSetupStep: React.FC = ({ onUpdate, onComplete, key={option.value} type="button" onClick={() => setFormData({ ...formData, role: option.value })} - className={`p-3 text-left border-2 rounded-lg transition-all ${ - formData.role === option.value - ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/20 shadow-lg ring-2 ring-[var(--color-primary)]/30' - : 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]' - }`} + className={`p - 3 text - left border - 2 rounded - lg transition - all ${formData.role === option.value + ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/20 shadow-lg ring-2 ring-[var(--color-primary)]/30' + : 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]' + } `} >
{option.icon} @@ -311,16 +310,29 @@ export const TeamSetupStep: React.FC = ({ onUpdate, onComplete,
)} - {/* Continue button - only shown when used in onboarding context */} - {onComplete && ( -
- + {/* Navigation buttons */} + {!isAdding && onComplete && ( +
+
+ +
+ +
+ +
)}
diff --git a/frontend/src/components/domain/setup-wizard/types.ts b/frontend/src/components/domain/setup-wizard/types.ts new file mode 100644 index 00000000..4577e1d3 --- /dev/null +++ b/frontend/src/components/domain/setup-wizard/types.ts @@ -0,0 +1,11 @@ +export interface SetupStepProps { + onNext?: () => void; + onPrevious?: () => void; + onComplete?: (data?: any) => void; + onUpdate?: (data?: any) => void; + onSkip?: () => void; + isFirstStep?: boolean; + isLastStep?: boolean; + itemCount?: number; + canContinue?: boolean; +} diff --git a/frontend/src/locales/en/common.json b/frontend/src/locales/en/common.json index ce8a2c6c..f829c1eb 100644 --- a/frontend/src/locales/en/common.json +++ b/frontend/src/locales/en/common.json @@ -425,7 +425,7 @@ "header": { "main_navigation": "Main navigation", "notifications": "Notifications", - "unread_count": "{{count}} unread notifications", + "unread_count": "{count} unread notifications", "login": "Login", "start_free": "Start Free", "register": "Sign Up", diff --git a/frontend/src/locales/en/dashboard.json b/frontend/src/locales/en/dashboard.json index cf897bb6..beaaf8d7 100644 --- a/frontend/src/locales/en/dashboard.json +++ b/frontend/src/locales/en/dashboard.json @@ -287,7 +287,7 @@ "user_needed": "User Needed", "needs_review": "needs your review", "all_handled": "all handled by AI", - "prevented_badge": "{{count}} issue{{count, plural, one {} other {s}}} prevented", + "prevented_badge": "{count} issue{{count, plural, one {} other {s}}} prevented", "prevented_description": "AI proactively handled these before they became problems", "analyzed_title": "What I Analyzed", "actions_taken": "What I Did", @@ -320,7 +320,7 @@ "celebration": "Great news! AI prevented {count} issue{plural} before they became problems.", "ai_insight": "AI Insight:", "show_less": "Show Less", - "show_more": "Show {{count}} More", + "show_more": "Show {count} More", "no_issues": "No issues prevented this week", "no_issues_detail": "All systems running smoothly!", "error_title": "Unable to load prevented issues" diff --git a/frontend/src/locales/en/help.json b/frontend/src/locales/en/help.json index c572b76a..2b710b76 100644 --- a/frontend/src/locales/en/help.json +++ b/frontend/src/locales/en/help.json @@ -7,8 +7,8 @@ "categoriesTitle": "Browse by Category", "categoriesSubtitle": "Find what you need faster", "faqTitle": "Frequently Asked Questions", - "faqResultsCount_one": "{{count}} answer", - "faqResultsCount_other": "{{count}} answers", + "faqResultsCount_one": "{count} answer", + "faqResultsCount_other": "{count} answers", "faqFound": "found", "noResultsTitle": "No results found for", "noResultsAction": "Contact support", diff --git a/frontend/src/locales/en/onboarding.json b/frontend/src/locales/en/onboarding.json index 63519447..2c139ba0 100644 --- a/frontend/src/locales/en/onboarding.json +++ b/frontend/src/locales/en/onboarding.json @@ -207,7 +207,7 @@ "skip_for_now": "Skip for now (will be set to 0)", "ingredients": "Ingredients", "finished_products": "Finished Products", - "incomplete_warning": "{{count}} products remaining", + "incomplete_warning": "{count} products remaining", "incomplete_help": "You can continue, but we recommend entering all quantities for better inventory control.", "complete": "Complete Setup", "continue_anyway": "Continue anyway", diff --git a/frontend/src/locales/en/setup_wizard.json b/frontend/src/locales/en/setup_wizard.json index fa408cda..d82fd8b3 100644 --- a/frontend/src/locales/en/setup_wizard.json +++ b/frontend/src/locales/en/setup_wizard.json @@ -24,8 +24,8 @@ }, "suppliers": { "why": "Suppliers are the source of your ingredients. Setting them up now lets you track costs, manage orders, and analyze supplier performance.", - "added_count": "{{count}} supplier added", - "added_count_plural": "{{count}} suppliers added", + "added_count": "{count} supplier added", + "added_count_plural": "{count} suppliers added", "minimum_met": "Minimum requirement met", "add_minimum": "Add at least 1 supplier to continue", "your_suppliers": "Your Suppliers", @@ -74,10 +74,10 @@ "import_all": "Import All", "templates_hint": "Click any item to customize before adding, or use \"Import All\" for quick setup", "show_templates": "Show Quick Start Templates", - "added_count": "{{count}} ingredient added", - "added_count_plural": "{{count}} ingredients added", + "added_count": "{count} ingredient added", + "added_count_plural": "{count} ingredients added", "minimum_met": "Minimum requirement met", - "need_more": "Need {{count}} more", + "need_more": "Need {count} more", "your_ingredients": "Your Ingredients", "add_ingredient": "Add Ingredient", "edit_ingredient": "Edit Ingredient", @@ -131,9 +131,9 @@ "show_templates": "Show Recipe Templates", "prerequisites_title": "More ingredients needed", "prerequisites_desc": "You need at least 2 ingredients in your inventory before creating recipes. Go back to the Inventory step to add more ingredients.", - "added_count": "{{count}} recipe added", - "added_count_plural": "{{count}} recipes added", - "minimum_met": "{{count}} recipe(s) added - Ready to continue!", + "added_count": "{count} recipe added", + "added_count_plural": "{count} recipes added", + "minimum_met": "{count} recipe(s) added - Ready to continue!", "your_recipes": "Your Recipes", "yield_label": "Yield", "add_recipe": "Add Recipe", @@ -167,8 +167,8 @@ "quality": { "why": "Quality checks ensure consistent output and help you identify issues early. Define what \"good\" looks like for each stage of production.", "optional_note": "You can skip this and configure quality checks later", - "added_count": "{{count}} quality check added", - "added_count_plural": "{{count}} quality checks added", + "added_count": "{count} quality check added", + "added_count_plural": "{count} quality checks added", "recommended_met": "Recommended amount met", "recommended": "2+ recommended (optional)", "your_checks": "Your Quality Checks", @@ -196,8 +196,8 @@ "why": "Adding team members allows you to assign tasks, track who does what, and give everyone the tools they need to work efficiently.", "optional_note": "You can add team members now or invite them later from settings", "invitation_note": "Team members will receive invitation emails once you complete the setup wizard.", - "added_count": "{{count}} team member added", - "added_count_plural": "{{count}} team members added", + "added_count": "{count} team member added", + "added_count_plural": "{count} team members added", "your_team": "Your Team Members", "add_member": "Add Team Member", "add_first": "Add Your First Team Member", diff --git a/frontend/src/locales/en/suppliers.json b/frontend/src/locales/en/suppliers.json index 057dd695..aa90296d 100644 --- a/frontend/src/locales/en/suppliers.json +++ b/frontend/src/locales/en/suppliers.json @@ -139,7 +139,7 @@ }, "price_list": { "title": "Product Price List", - "subtitle": "{{count}} products available from this supplier", + "subtitle": "{count} products available from this supplier", "modal": { "title_create": "Add Product to Supplier", "title_edit": "Edit Product Price", diff --git a/frontend/src/locales/es/auth.json b/frontend/src/locales/es/auth.json index ad95bf9b..8e51b02a 100644 --- a/frontend/src/locales/es/auth.json +++ b/frontend/src/locales/es/auth.json @@ -65,7 +65,7 @@ "total_today": "Total hoy:", "payment_required": "Tarjeta requerida para validación", "billing_message": "Se te cobrará {{price}} después del período de prueba", - "free_months": "{{count}} meses GRATIS", + "free_months": "{count} meses GRATIS", "free_days": "14 días gratis", "payment_info": "Información de Pago", "secure_payment": "Tu información de pago está protegida con encriptación de extremo a extremo", diff --git a/frontend/src/locales/es/common.json b/frontend/src/locales/es/common.json index f00ca96b..60407b02 100644 --- a/frontend/src/locales/es/common.json +++ b/frontend/src/locales/es/common.json @@ -447,7 +447,7 @@ "header": { "main_navigation": "Navegación principal", "notifications": "Notificaciones", - "unread_count": "{{count}} notificaciones sin leer", + "unread_count": "{count} notificaciones sin leer", "login": "Iniciar Sesión", "start_free": "Comenzar Gratis", "register": "Registro", diff --git a/frontend/src/locales/es/dashboard.json b/frontend/src/locales/es/dashboard.json index f0c972bd..ed6d8c53 100644 --- a/frontend/src/locales/es/dashboard.json +++ b/frontend/src/locales/es/dashboard.json @@ -131,8 +131,8 @@ "view_all": "Ver todas las alertas", "time": { "now": "Ahora", - "minutes_ago": "hace {{count}} min", - "hours_ago": "hace {{count}} h", + "minutes_ago": "hace {count} min", + "hours_ago": "hace {count} h", "yesterday": "Ayer" }, "types": { @@ -173,7 +173,7 @@ "remove": "Eliminar", "snooze": "Posponer", "unsnooze": "Reactivar", - "active_count": "{{count}} alertas activas", + "active_count": "{count} alertas activas", "empty_state": { "no_results": "Sin resultados", "all_clear": "Todo despejado", @@ -264,7 +264,7 @@ "suppliers": "Proveedores", "recipes": "Recetas", "quality": "Estándares de Calidad", - "add_ingredients": "Agregar al menos {{count}} ingredientes", + "add_ingredients": "Agregar al menos {count} ingredientes", "add_supplier": "Agregar tu primer proveedor", "add_recipe": "Crear tu primera receta", "add_quality": "Agregar controles de calidad (opcional)", @@ -336,7 +336,7 @@ "user_needed": "Usuario Necesario", "needs_review": "necesita tu revisión", "all_handled": "todo manejado por IA", - "prevented_badge": "{{count}} problema{{count, plural, one {} other {s}}} evitado{{count, plural, one {} other {s}}}", + "prevented_badge": "{count} problema{{count, plural, one {} other {s}}} evitado{{count, plural, one {} other {s}}}", "prevented_description": "La IA manejó estos proactivamente antes de que se convirtieran en problemas", "analyzed_title": "Lo Que Analicé", "actions_taken": "Lo Que Hice", @@ -369,7 +369,7 @@ "celebration": "¡Buenas noticias! La IA evitó {count} incidencia{plural} antes de que se convirtieran en problemas.", "ai_insight": "Análisis de IA:", "show_less": "Mostrar Menos", - "show_more": "Mostrar {{count}} Más", + "show_more": "Mostrar {count} Más", "no_issues": "No se evitaron incidencias esta semana", "no_issues_detail": "¡Todos los sistemas funcionan correctamente!", "error_title": "No se pueden cargar las incidencias evitadas" diff --git a/frontend/src/locales/es/help.json b/frontend/src/locales/es/help.json index e64abb75..a9bf22cf 100644 --- a/frontend/src/locales/es/help.json +++ b/frontend/src/locales/es/help.json @@ -7,8 +7,8 @@ "categoriesTitle": "Explora por Categoría", "categoriesSubtitle": "Encuentra lo que necesitas más rápido", "faqTitle": "Preguntas Frecuentes", - "faqResultsCount_one": "{{count}} respuesta", - "faqResultsCount_other": "{{count}} respuestas", + "faqResultsCount_one": "{count} respuesta", + "faqResultsCount_other": "{count} respuestas", "faqFound": "encontradas", "noResultsTitle": "No encontramos resultados para", "noResultsAction": "Contacta con soporte", diff --git a/frontend/src/locales/es/inventory.json b/frontend/src/locales/es/inventory.json index 30eaa23d..b49a9890 100644 --- a/frontend/src/locales/es/inventory.json +++ b/frontend/src/locales/es/inventory.json @@ -241,7 +241,7 @@ "save_item": "Guardar Artículo", "cancel": "Cancelar", "delete_confirmation": "¿Estás seguro de que quieres eliminar este artículo?", - "bulk_update_confirmation": "¿Estás seguro de que quieres actualizar {{count}} artículos?" + "bulk_update_confirmation": "¿Estás seguro de que quieres actualizar {count} artículos?" }, "reports": { "inventory_report": "Reporte de Inventario", diff --git a/frontend/src/locales/es/onboarding.json b/frontend/src/locales/es/onboarding.json index 274af74c..c6206587 100644 --- a/frontend/src/locales/es/onboarding.json +++ b/frontend/src/locales/es/onboarding.json @@ -336,7 +336,7 @@ "add_new": "Nuevo Proceso", "add_button": "Agregar Proceso", "hint": "💡 Agrega al menos un proceso para continuar", - "count": "{{count}} proceso(s) configurado(s)", + "count": "{count} proceso(s) configurado(s)", "skip": "Omitir por ahora", "continue": "Continuar", "source": "Desde", diff --git a/frontend/src/locales/es/setup_wizard.json b/frontend/src/locales/es/setup_wizard.json index d19ca1cc..dad879e1 100644 --- a/frontend/src/locales/es/setup_wizard.json +++ b/frontend/src/locales/es/setup_wizard.json @@ -24,8 +24,8 @@ }, "suppliers": { "why": "Los proveedores son la fuente de tus ingredientes. Configurarlos ahora te permite rastrear costos, gestionar pedidos y analizar el rendimiento de los proveedores.", - "added_count": "{{count}} proveedor agregado", - "added_count_plural": "{{count}} proveedores agregados", + "added_count": "{count} proveedor agregado", + "added_count_plural": "{count} proveedores agregados", "minimum_met": "Requisito mínimo cumplido", "add_minimum": "Agrega al menos 1 proveedor para continuar", "your_suppliers": "Tus Proveedores", @@ -74,10 +74,10 @@ "import_all": "Importar Todo", "templates_hint": "Haz clic en cualquier artículo para personalizarlo antes de agregarlo, o usa \"Importar Todo\" para una configuración rápida", "show_templates": "Mostrar Plantillas de Inicio Rápido", - "added_count": "{{count}} ingrediente agregado", - "added_count_plural": "{{count}} ingredientes agregados", + "added_count": "{count} ingrediente agregado", + "added_count_plural": "{count} ingredientes agregados", "minimum_met": "Requisito mínimo cumplido", - "need_more": "Necesitas {{count}} más", + "need_more": "Necesitas {count} más", "your_ingredients": "Tus Ingredientes", "add_ingredient": "Agregar Ingrediente", "edit_ingredient": "Editar Ingrediente", @@ -131,9 +131,9 @@ "show_templates": "Mostrar Plantillas de Recetas", "prerequisites_title": "Se necesitan más ingredientes", "prerequisites_desc": "Necesitas al menos 2 ingredientes en tu inventario antes de crear recetas. Regresa al paso de Inventario para agregar más ingredientes.", - "added_count": "{{count}} receta agregada", - "added_count_plural": "{{count}} recetas agregadas", - "minimum_met": "{{count}} receta(s) agregada(s) - ¡Listo para continuar!", + "added_count": "{count} receta agregada", + "added_count_plural": "{count} recetas agregadas", + "minimum_met": "{count} receta(s) agregada(s) - ¡Listo para continuar!", "your_recipes": "Tus Recetas", "yield_label": "Rendimiento", "add_recipe": "Agregar Receta", @@ -167,8 +167,8 @@ "quality": { "why": "Los controles de calidad aseguran una producción consistente y te ayudan a identificar problemas temprano. Define qué significa \"bueno\" para cada etapa de producción.", "optional_note": "Puedes omitir esto y configurar los controles de calidad más tarde", - "added_count": "{{count}} control de calidad agregado", - "added_count_plural": "{{count}} controles de calidad agregados", + "added_count": "{count} control de calidad agregado", + "added_count_plural": "{count} controles de calidad agregados", "recommended_met": "Cantidad recomendada cumplida", "recommended": "2+ recomendados (opcional)", "your_checks": "Tus Controles de Calidad", @@ -196,8 +196,8 @@ "why": "Agregar miembros del equipo te permite asignar tareas, rastrear quién hace qué y dar a todos las herramientas que necesitan para trabajar eficientemente.", "optional_note": "Puedes agregar miembros del equipo ahora o invitarlos más tarde desde la configuración", "invitation_note": "Los miembros del equipo recibirán correos de invitación una vez que completes el asistente de configuración.", - "added_count": "{{count}} miembro del equipo agregado", - "added_count_plural": "{{count}} miembros del equipo agregados", + "added_count": "{count} miembro del equipo agregado", + "added_count_plural": "{count} miembros del equipo agregados", "your_team": "Los Miembros de tu Equipo", "add_member": "Agregar Miembro del Equipo", "add_first": "Agrega tu Primer Miembro del Equipo", diff --git a/frontend/src/locales/es/suppliers.json b/frontend/src/locales/es/suppliers.json index 68cf4137..81569c08 100644 --- a/frontend/src/locales/es/suppliers.json +++ b/frontend/src/locales/es/suppliers.json @@ -139,7 +139,7 @@ }, "price_list": { "title": "Lista de Precios de Productos", - "subtitle": "{{count}} productos disponibles de este proveedor", + "subtitle": "{count} productos disponibles de este proveedor", "modal": { "title_create": "Añadir Producto al Proveedor", "title_edit": "Editar Precio de Producto", diff --git a/frontend/src/locales/eu/auth.json b/frontend/src/locales/eu/auth.json index d6c31d92..be488e98 100644 --- a/frontend/src/locales/eu/auth.json +++ b/frontend/src/locales/eu/auth.json @@ -63,7 +63,7 @@ "total_today": "Gaurko totala:", "payment_required": "Ordainketa beharrezkoa balidaziorako", "billing_message": "{{price}} kobratuko zaizu proba epea ondoren", - "free_months": "{{count}} hilabete DOAN", + "free_months": "{count} hilabete DOAN", "free_days": "14 egun doan", "payment_info": "Ordainketaren informazioa", "secure_payment": "Zure ordainketa informazioa babespetuta dago amaieratik amaierarako zifratzearekin", diff --git a/frontend/src/locales/eu/dashboard.json b/frontend/src/locales/eu/dashboard.json index 20506583..5d72aa5e 100644 --- a/frontend/src/locales/eu/dashboard.json +++ b/frontend/src/locales/eu/dashboard.json @@ -248,7 +248,7 @@ "user_needed": "Erabiltzailea Behar", "needs_review": "zure berrikuspena behar du", "all_handled": "guztia AIak kudeatua", - "prevented_badge": "{{count}} arazu saihestau{{count, plural, one {} other {}}", + "prevented_badge": "{count} arazu saihestau{{count, plural, one {} other {}}", "prevented_description": "AIak hauek proaktiboki kudeatu zituen arazo bihurtu aurretik", "analyzed_title": "Zer Aztertu Nuen", "actions_taken": "Zer Egin Nuen", @@ -281,7 +281,7 @@ "celebration": "Albiste onak! AIk {count} arazu{plural} saihestau ditu arazo bihurtu aurretik.", "ai_insight": "AI Analisia:", "show_less": "Gutxiago Erakutsi", - "show_more": "{{count}} Gehiago Erakutsi", + "show_more": "{count} Gehiago Erakutsi", "no_issues": "Ez da arazorik saihestau aste honetan", "no_issues_detail": "Sistema guztiak ondo dabiltza!", "error_title": "Ezin dira saihestutako arazoak kargatu" diff --git a/frontend/src/locales/eu/help.json b/frontend/src/locales/eu/help.json index 2946e006..adeb0f0e 100644 --- a/frontend/src/locales/eu/help.json +++ b/frontend/src/locales/eu/help.json @@ -7,8 +7,8 @@ "categoriesTitle": "Arakatu Kategorien arabera", "categoriesSubtitle": "Aurkitu behar duzuna azkarrago", "faqTitle": "Ohiko Galderak", - "faqResultsCount_one": "{{count}} erantzun", - "faqResultsCount_other": "{{count}} erantzun", + "faqResultsCount_one": "{count} erantzun", + "faqResultsCount_other": "{count} erantzun", "faqFound": "aurkituta", "noResultsTitle": "Ez da emaitzarik aurkitu honetarako", "noResultsAction": "Jarri harremanetan laguntza-zerbitzuarekin", diff --git a/frontend/src/locales/eu/onboarding.json b/frontend/src/locales/eu/onboarding.json index 179fb19a..c04a1629 100644 --- a/frontend/src/locales/eu/onboarding.json +++ b/frontend/src/locales/eu/onboarding.json @@ -318,7 +318,7 @@ "add_new": "Prozesu Berria", "add_button": "Prozesua Gehitu", "hint": "💡 Gehitu gutxienez prozesu bat jarraitzeko", - "count": "{{count}} prozesu konfiguratuta", + "count": "{count} prozesu konfiguratuta", "skip": "Oraingoz saltatu", "continue": "Jarraitu", "source": "Hemendik", @@ -379,7 +379,7 @@ "skip_for_now": "Oraingoz saltatu (0an ezarriko da)", "ingredients": "Osagaiak", "finished_products": "Produktu Amaituak", - "incomplete_warning": "{{count}} produktu osatu gabe geratzen dira", + "incomplete_warning": "{count} produktu osatu gabe geratzen dira", "incomplete_help": "Jarraitu dezakezu, baina kopuru guztiak sartzea gomendatzen dizugu inbentario-kontrol hobeagorako.", "complete": "Konfigurazioa Osatu", "continue_anyway": "Jarraitu hala ere", diff --git a/frontend/src/locales/eu/setup_wizard.json b/frontend/src/locales/eu/setup_wizard.json index 337c5d83..c0d391d6 100644 --- a/frontend/src/locales/eu/setup_wizard.json +++ b/frontend/src/locales/eu/setup_wizard.json @@ -24,8 +24,8 @@ }, "suppliers": { "why": "Hornitzaileak zure osagaien iturria dira. Orain konfiguratuz, kostuak jarraitu, eskaerak kudeatu eta hornitzaileen errendimendua aztertu dezakezu.", - "added_count": "Hornitzaile {{count}} gehituta", - "added_count_plural": "{{count}} hornitzaile gehituta", + "added_count": "Hornitzaile {count} gehituta", + "added_count_plural": "{count} hornitzaile gehituta", "minimum_met": "Gutxieneko baldintza betetzen da", "add_minimum": "Gehitu gutxienez hornitzaile 1 jarraitzeko", "your_suppliers": "Zure Hornitzaileak", @@ -74,10 +74,10 @@ "import_all": "Dena Inportatu", "templates_hint": "Klik egin edozein elementutan gehitu aurretik pertsonalizatzeko, edo erabili \"Dena Inportatu\" konfigurazio azkarrerako", "show_templates": "Erakutsi Abio Azkarreko Txantiloiak", - "added_count": "Osagai {{count}} gehituta", - "added_count_plural": "{{count}} osagai gehituta", + "added_count": "Osagai {count} gehituta", + "added_count_plural": "{count} osagai gehituta", "minimum_met": "Gutxieneko baldintza betetzen da", - "need_more": "{{count}} gehiago behar dira", + "need_more": "{count} gehiago behar dira", "your_ingredients": "Zure Osagaiak", "add_ingredient": "Osagaia Gehitu", "edit_ingredient": "Osagaia Editatu", @@ -131,9 +131,9 @@ "show_templates": "Erakutsi Errezeta Txantiloiak", "prerequisites_title": "Osagai gehiago behar dira", "prerequisites_desc": "Gutxienez 2 osagai behar dituzu zure inbentarioan errezetak sortu aurretik. Itzuli Inbentario urratsera osagai gehiago gehitzeko.", - "added_count": "Errezeta {{count}} gehituta", - "added_count_plural": "{{count}} errezeta gehituta", - "minimum_met": "{{count}} errezeta gehituta - Jarraitzeko prest!", + "added_count": "Errezeta {count} gehituta", + "added_count_plural": "{count} errezeta gehituta", + "minimum_met": "{count} errezeta gehituta - Jarraitzeko prest!", "your_recipes": "Zure Errezetak", "yield_label": "Etekin", "add_recipe": "Errezeta Gehitu", @@ -167,8 +167,8 @@ "quality": { "why": "Kalitate kontrolek irteera koherentea bermatzen dute eta goiz arazoak identifikatzen laguntzen dizute. Definitu zer den \"ona\" ekoizpen etapa bakoitzerako.", "optional_note": "Hau saltatu eta kalitate kontrolak geroago konfigura ditzakezu", - "added_count": "Kalitate kontrol {{count}} gehituta", - "added_count_plural": "{{count}} kalitate kontrol gehituta", + "added_count": "Kalitate kontrol {count} gehituta", + "added_count_plural": "{count} kalitate kontrol gehituta", "recommended_met": "Gomendatutako kopurua betetzen da", "recommended": "2+ gomendatzen dira (aukerakoa)", "your_checks": "Zure Kalitate Kontrolak", @@ -196,8 +196,8 @@ "why": "Taldekideak gehitzeak zereginak esleitzea, nork zer egiten duen jarraitzea eta guztiei behar dituzten tresnak ematea ahalbidetzen dizu modu eraginkorrean lan egiteko.", "optional_note": "Taldekideak orain gehi ditzakezu edo ezarpenetatik geroago gonbida ditzakezu", "invitation_note": "Taldekideek gonbidapen posta elektronikoak jasoko dituzte konfigurazio morroia osatu ondoren.", - "added_count": "Taldekide {{count}} gehituta", - "added_count_plural": "{{count}} taldekide gehituta", + "added_count": "Taldekide {count} gehituta", + "added_count_plural": "{count} taldekide gehituta", "your_team": "Zure Taldekideak", "add_member": "Taldekidea Gehitu", "add_first": "Gehitu Zure Lehen Taldekidea", diff --git a/frontend/src/locales/eu/suppliers.json b/frontend/src/locales/eu/suppliers.json index 2a1f2fb2..0eb75ea1 100644 --- a/frontend/src/locales/eu/suppliers.json +++ b/frontend/src/locales/eu/suppliers.json @@ -139,7 +139,7 @@ }, "price_list": { "title": "Produktuen Prezioen Zerrenda", - "subtitle": "{{count}} produktu hornitzaile honetatik eskuragarri", + "subtitle": "{count} produktu hornitzaile honetatik eskuragarri", "modal": { "title_create": "Produktua Gehitu Hornitzaileari", "title_edit": "Produktuaren Prezioa Editatu", diff --git a/frontend/src/pages/app/DashboardPage.tsx b/frontend/src/pages/app/DashboardPage.tsx index 55cbf399..f6accbb6 100644 --- a/frontend/src/pages/app/DashboardPage.tsx +++ b/frontend/src/pages/app/DashboardPage.tsx @@ -16,9 +16,11 @@ */ import { useState, useEffect, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; import { Plus, Sparkles } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useTenant } from '../../stores/tenant.store'; +import { useIsAuthenticated } from '../../stores'; import { useApprovePurchaseOrder, useStartProductionBatch, @@ -28,6 +30,7 @@ import { useRejectPurchaseOrder } from '../../api/hooks/purchase-orders'; import { useIngredients } from '../../api/hooks/inventory'; import { useSuppliers } from '../../api/hooks/suppliers'; import { useRecipes } from '../../api/hooks/recipes'; +import { useUserProgress } from '../../api/hooks/onboarding'; import { useQualityTemplates } from '../../api/hooks/qualityTemplates'; import { SetupWizardBlocker } from '../../components/dashboard/SetupWizardBlocker'; import { CollapsibleSetupBanner } from '../../components/dashboard/CollapsibleSetupBanner'; @@ -482,9 +485,39 @@ export function BakeryDashboard({ plan }: { plan?: string }) { export function DashboardPage() { const { subscriptionInfo } = useSubscription(); const { currentTenant } = useTenant(); - const { plan, loading } = subscriptionInfo; + const navigate = useNavigate(); + const { plan, loading: subLoading } = subscriptionInfo; const tenantId = currentTenant?.id; + // Fetch onboarding progress + const isAuthenticated = useIsAuthenticated(); + const { data: userProgress, isLoading: progressLoading } = useUserProgress('', { + enabled: !!isAuthenticated && plan !== SUBSCRIPTION_TIERS.ENTERPRISE + }); + + const loading = subLoading || progressLoading; + + useEffect(() => { + if (!loading && userProgress && !userProgress.fully_completed && plan !== SUBSCRIPTION_TIERS.ENTERPRISE) { + // CRITICAL: Check if user is on the completion step + // If they are, don't redirect (they're in the process of completing onboarding) + const isOnCompletionStep = userProgress.current_step === 'completion'; + + if (!isOnCompletionStep) { + console.log('🔄 Onboarding incomplete, redirecting to wizard...', { + currentStep: userProgress.current_step, + fullyCompleted: userProgress.fully_completed + }); + navigate('/app/onboarding'); + } else { + console.log('✅ User on completion step, allowing dashboard access', { + currentStep: userProgress.current_step, + fullyCompleted: userProgress.fully_completed + }); + } + } + }, [loading, userProgress, plan, navigate]); + if (loading) { return (
diff --git a/frontend/src/styles/animations.css b/frontend/src/styles/animations.css index 65660867..06deb58c 100644 --- a/frontend/src/styles/animations.css +++ b/frontend/src/styles/animations.css @@ -731,4 +731,63 @@ .animate-shimmer { animation: shimmer 2s ease-in-out infinite; +} + +/* Onboarding-specific animations */ +@keyframes bounce-subtle { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-5px); + } +} + +@keyframes slide-up { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes stagger-in { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes pulse-slow { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.85; + transform: scale(1.05); + } +} + +.animate-bounce-subtle { + animation: bounce-subtle 3s ease-in-out infinite; +} + +.animate-slide-up { + animation: slide-up 0.5s ease-out; +} + +.animate-stagger-in { + animation: stagger-in 0.6s ease-out; +} + +.animate-pulse-slow { + animation: pulse-slow 2s ease-in-out infinite; } \ No newline at end of file diff --git a/services/auth/app/api/onboarding_progress.py b/services/auth/app/api/onboarding_progress.py index fe6a003c..4fe94245 100644 --- a/services/auth/app/api/onboarding_progress.py +++ b/services/auth/app/api/onboarding_progress.py @@ -124,38 +124,63 @@ class OnboardingService: async def get_user_progress(self, user_id: str) -> UserProgress: """Get current onboarding progress for user""" - + # Get user's onboarding data from user preferences or separate table user_progress_data = await self._get_user_onboarding_data(user_id) - + # Calculate current status for each step steps = [] completed_steps = [] - + for step_name in ONBOARDING_STEPS: step_data = user_progress_data.get(step_name, {}) is_completed = step_data.get("completed", False) - + if is_completed: completed_steps.append(step_name) - + steps.append(OnboardingStepStatus( step_name=step_name, completed=is_completed, completed_at=step_data.get("completed_at"), data=step_data.get("data", {}) )) - + # Determine current and next step current_step = self._get_current_step(completed_steps) next_step = self._get_next_step(completed_steps) - + # Calculate completion percentage completion_percentage = (len(completed_steps) / len(ONBOARDING_STEPS)) * 100 - - # Check if fully completed - fully_completed = len(completed_steps) == len(ONBOARDING_STEPS) - + + # Check if fully completed - based on REQUIRED steps only + # Define required steps + REQUIRED_STEPS = [ + "user_registered", + "setup", + "suppliers-setup", + "ml-training", + "completion" + ] + + # Get user's subscription tier to determine if bakery-type-selection is required + user_registered_data = user_progress_data.get("user_registered", {}).get("data", {}) + subscription_tier = user_registered_data.get("subscription_tier", "professional") + + # Add bakery-type-selection to required steps for non-enterprise users + if subscription_tier != "enterprise": + required_steps_for_user = REQUIRED_STEPS + ["bakery-type-selection"] + else: + required_steps_for_user = REQUIRED_STEPS + + # Check if all required steps are completed + required_completed = all( + user_progress_data.get(step, {}).get("completed", False) + for step in required_steps_for_user + ) + + fully_completed = required_completed + return UserProgress( user_id=user_id, steps=steps, @@ -234,27 +259,77 @@ class OnboardingService: async def complete_onboarding(self, user_id: str) -> Dict[str, Any]: """Mark entire onboarding as complete""" - - # Ensure all steps are completed + + # Get user's progress progress = await self.get_user_progress(user_id) - - if not progress.fully_completed: - incomplete_steps = [ - step.step_name for step in progress.steps if not step.completed - ] + user_progress_data = await self._get_user_onboarding_data(user_id) + + # Define REQUIRED steps (excluding optional/conditional steps) + # These are the minimum steps needed to complete onboarding + REQUIRED_STEPS = [ + "user_registered", + "setup", # bakery-type-selection is conditional for enterprise + "suppliers-setup", + "ml-training", + "completion" + ] + + # Define CONDITIONAL steps that are only required for certain tiers/flows + CONDITIONAL_STEPS = { + "child-tenants-setup": "enterprise", # Only for enterprise tier + "product-categorization": None, # Optional for all + "bakery-type-selection": "non-enterprise", # Only for non-enterprise + "upload-sales-data": None, # Optional (manual inventory setup is alternative) + "inventory-review": None, # Optional (manual inventory setup is alternative) + "initial-stock-entry": None, # Optional + "recipes-setup": None, # Optional + "quality-setup": None, # Optional + "team-setup": None, # Optional + } + + # Get user's subscription tier + user_registered_data = user_progress_data.get("user_registered", {}).get("data", {}) + subscription_tier = user_registered_data.get("subscription_tier", "professional") + + # Check if all REQUIRED steps are completed + incomplete_required_steps = [] + for step_name in REQUIRED_STEPS: + if not user_progress_data.get(step_name, {}).get("completed", False): + # Special case: bakery-type-selection is not required for enterprise + if step_name == "bakery-type-selection" and subscription_tier == "enterprise": + continue + incomplete_required_steps.append(step_name) + + # If there are incomplete required steps, reject completion + if incomplete_required_steps: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Cannot complete onboarding: incomplete steps: {incomplete_steps}" + detail=f"Cannot complete onboarding: incomplete required steps: {incomplete_required_steps}" ) - + + # Log conditional steps that are incomplete (warning only, not blocking) + incomplete_conditional_steps = [ + step.step_name for step in progress.steps + if not step.completed and step.step_name in CONDITIONAL_STEPS + ] + if incomplete_conditional_steps: + logger.info( + f"User {user_id} completing onboarding with incomplete optional steps: {incomplete_conditional_steps}", + extra={"user_id": user_id, "subscription_tier": subscription_tier} + ) + # Update user's isOnboardingComplete flag await self.user_service.update_user_field( - user_id, - "is_onboarding_complete", + user_id, + "is_onboarding_complete", True ) - - return {"success": True, "message": "Onboarding completed successfully"} + + return { + "success": True, + "message": "Onboarding completed successfully", + "optional_steps_skipped": incomplete_conditional_steps + } def _get_current_step(self, completed_steps: List[str]) -> str: """Determine current step based on completed steps"""