+
+ ✨
{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'
+ )}
+
+
- {t('onboarding:bakery_type.continue_button', 'Continuar')}
+ {t('onboarding:bakery_type.continue_button', 'Continuar')} →
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 */}
-
+
navigate('/app/dashboard')}
- className="p-4 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] rounded-lg transition-all hover:shadow-lg hover:scale-105 text-left group"
+ className="p-5 md:p-6 bg-gradient-to-br from-[var(--bg-secondary)] to-[var(--bg-primary)] hover:from-[var(--bg-tertiary)] hover:to-[var(--bg-secondary)] border-2 border-[var(--border-secondary)] rounded-xl transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:-translate-y-1 text-left group"
>
-
-
+
+
{t('onboarding:completion.quick.analytics', 'Analíticas')}
-
+
{t('onboarding:completion.quick.analytics_desc', 'Ver predicciones y métricas')}
navigate('/app/inventory')}
- className="p-4 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] rounded-lg transition-all hover:shadow-lg hover:scale-105 text-left group"
+ className="p-5 md:p-6 bg-gradient-to-br from-[var(--bg-secondary)] to-[var(--bg-primary)] hover:from-[var(--bg-tertiary)] hover:to-[var(--bg-secondary)] border-2 border-[var(--border-secondary)] rounded-xl transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:-translate-y-1 text-left group"
>
-
-
+
+
{t('onboarding:completion.quick.inventory', 'Inventario')}
-
+
{t('onboarding:completion.quick.inventory_desc', 'Gestionar stock y productos')}
navigate('/app/procurement')}
- className="p-4 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] rounded-lg transition-all hover:shadow-lg hover:scale-105 text-left group"
+ className="p-5 md:p-6 bg-gradient-to-br from-[var(--bg-secondary)] to-[var(--bg-primary)] hover:from-[var(--bg-tertiary)] hover:to-[var(--bg-secondary)] border-2 border-[var(--border-secondary)] rounded-xl transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:-translate-y-1 text-left group"
>
-
-
+
+
{t('onboarding:completion.quick.procurement', 'Compras')}
-
+
{t('onboarding:completion.quick.procurement_desc', 'Gestionar pedidos')}
navigate('/app/production')}
- className="p-4 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] rounded-lg transition-all hover:shadow-lg hover:scale-105 text-left group"
+ className="p-5 md:p-6 bg-gradient-to-br from-[var(--bg-secondary)] to-[var(--bg-primary)] hover:from-[var(--bg-tertiary)] hover:to-[var(--bg-secondary)] border-2 border-[var(--border-secondary)] rounded-xl transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:-translate-y-1 text-left group"
>
-
-
+
+
{t('onboarding:completion.quick.production', 'Producción')}
-
+
{t('onboarding:completion.quick.production_desc', 'Planificar producción')}
{/* 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 */}
-
+
- {t('onboarding:completion.go_to_dashboard', 'Comenzar a Usar el Sistema →')}
+ {t('onboarding:completion.go_to_dashboard', 'Comenzar a Usar el Sistema')} 🚀
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 = ({
setActiveFilter('all')}
- className={`px-3 md:px-4 py-3 font-medium transition-colors relative whitespace-nowrap text-sm md:text-base ${
- activeFilter === 'all'
- ? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)] -mb-px'
- : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
- }`}
+ className={`px-3 md:px-4 py-3 font-medium transition-colors relative whitespace-nowrap text-sm md:text-base ${activeFilter === 'all'
+ ? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)] -mb-px'
+ : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
+ }`}
>
{t('inventory:filter.all', 'Todos')} ({counts.all})
setActiveFilter('finished_products')}
- className={`px-3 md:px-4 py-3 font-medium transition-colors relative flex items-center gap-2 whitespace-nowrap text-sm md:text-base ${
- activeFilter === 'finished_products'
- ? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)] -mb-px'
- : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
- }`}
+ className={`px-3 md:px-4 py-3 font-medium transition-colors relative flex items-center gap-2 whitespace-nowrap text-sm md:text-base ${activeFilter === 'finished_products'
+ ? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)] -mb-px'
+ : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
+ }`}
>
{t('inventory:filter.finished_products', 'Productos Terminados')}
@@ -461,11 +506,10 @@ export const InventoryReviewStep: React.FC = ({
setActiveFilter('ingredients')}
- className={`px-3 md:px-4 py-3 font-medium transition-colors relative flex items-center gap-2 whitespace-nowrap text-sm md:text-base ${
- activeFilter === 'ingredients'
- ? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)] -mb-px'
- : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
- }`}
+ className={`px-3 md:px-4 py-3 font-medium transition-colors relative flex items-center gap-2 whitespace-nowrap text-sm md:text-base ${activeFilter === 'ingredients'
+ ? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)] -mb-px'
+ : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
+ }`}
>
{t('inventory:filter.ingredients', 'Ingredientes')} ({counts.ingredients})
@@ -492,11 +536,10 @@ export const InventoryReviewStep: React.FC = ({
{item.name}
{/* Product Type Badge */}
-
+
{item.product_type === ProductType.FINISHED_PRODUCT ? (
@@ -579,11 +622,10 @@ export const InventoryReviewStep: React.FC = ({
setFormData(prev => ({ ...prev, product_type: ProductType.INGREDIENT, category: '' }))}
- className={`relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${
- formData.product_type === ProductType.INGREDIENT
- ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
- : 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
- }`}
+ className={`relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${formData.product_type === ProductType.INGREDIENT
+ ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
+ : 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
+ }`}
>
{formData.product_type === ProductType.INGREDIENT && (
@@ -600,11 +642,10 @@ export const InventoryReviewStep: React.FC = ({
setFormData(prev => ({ ...prev, product_type: ProductType.FINISHED_PRODUCT, category: '' }))}
- className={`relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${
- formData.product_type === ProductType.FINISHED_PRODUCT
- ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
- : 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
- }`}
+ className={`relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${formData.product_type === ProductType.FINISHED_PRODUCT
+ ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
+ : 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
+ }`}
>
{formData.product_type === ProductType.FINISHED_PRODUCT && (
@@ -724,11 +765,10 @@ export const InventoryReviewStep: React.FC = ({
setFormData(prev => ({ ...prev, product_type: ProductType.INGREDIENT, category: '' }))}
- className={`relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${
- formData.product_type === ProductType.INGREDIENT
- ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
- : 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
- }`}
+ className={`relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${formData.product_type === ProductType.INGREDIENT
+ ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
+ : 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
+ }`}
>
{formData.product_type === ProductType.INGREDIENT && (
@@ -745,11 +785,10 @@ export const InventoryReviewStep: React.FC = ({
setFormData(prev => ({ ...prev, product_type: ProductType.FINISHED_PRODUCT, category: '' }))}
- className={`relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${
- formData.product_type === ProductType.FINISHED_PRODUCT
- ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
- : 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
- }`}
+ className={`relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${formData.product_type === ProductType.FINISHED_PRODUCT
+ ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
+ : 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
+ }`}
>
{formData.product_type === ProductType.FINISHED_PRODUCT && (
@@ -862,9 +901,9 @@ export const InventoryReviewStep: React.FC = ({
{isSubmitting
? t('common:saving', 'Guardando...')
: <>
- {t('common:continue', 'Continuar')} ({inventoryItems.length} {t('common:items', 'productos')}) →
- {t('common:continue', 'Continuar')} ({inventoryItems.length}) →
- >
+ {t('common:continue', 'Continuar')} ({inventoryItems.length} {t('common:items', 'productos')}) →
+ {t('common:continue', 'Continuar')} ({inventoryItems.length}) →
+ >
}
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
+ />
+
-
-
+
+ handleInputChange('phone', e.target.value)}
+ error={errors.phone}
+ isRequired
+ />
+
+
+
{errors.submit && (
-
- {errors.submit}
+
+
+
⚠️
+
+
Error al registrar
+
{errors.submit}
+
+
)}
-
+
- {isEnterprise ? "Crear Obrador Central y Continuar" : "Crear Panadería y Continuar"}
+ {tenantId
+ ? (isEnterprise ? "Actualizar Obrador Central y Continuar →" : "Actualizar Panadería y Continuar →")
+ : (isEnterprise ? "Crear Obrador Central y Continuar →" : "Crear Panadería y Continuar →")
+ }
diff --git a/frontend/src/components/domain/setup-wizard/components/StepNavigation.tsx b/frontend/src/components/domain/setup-wizard/components/StepNavigation.tsx
index 75d6bc0c..0a88eb12 100644
--- a/frontend/src/components/domain/setup-wizard/components/StepNavigation.tsx
+++ b/frontend/src/components/domain/setup-wizard/components/StepNavigation.tsx
@@ -113,7 +113,7 @@ export const StepNavigation: React.FC
= ({
{!canContinue && !currentStep.isOptional && !isLastStep && (
{currentStep.minRequired && currentStep.minRequired > 0 ? (
- t('setup_wizard:min_required', 'Add at least {{count}} {{itemType}} to continue', {
+ t('setup_wizard:min_required', 'Add at least {count} {{itemType}} to continue', {
count: currentStep.minRequired,
itemType: getItemType(currentStep.id)
})
diff --git a/frontend/src/components/domain/setup-wizard/steps/InventorySetupStep.tsx b/frontend/src/components/domain/setup-wizard/steps/InventorySetupStep.tsx
index b864fd46..5b835cb8 100644
--- a/frontend/src/components/domain/setup-wizard/steps/InventorySetupStep.tsx
+++ b/frontend/src/components/domain/setup-wizard/steps/InventorySetupStep.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';
import { useIngredients, useCreateIngredient, useUpdateIngredient, useSoftDeleteIngredient, useAddStock, useStockByIngredient } from '../../../../api/hooks/inventory';
import { useSuppliers } from '../../../../api/hooks/suppliers';
import { useCurrentTenant } from '../../../../stores/tenant.store';
@@ -9,7 +9,7 @@ import { UnitOfMeasure, IngredientCategory, ProductionStage } from '../../../../
import type { IngredientCreate, IngredientUpdate, StockCreate, StockResponse } from '../../../../api/types/inventory';
import { ESSENTIAL_INGREDIENTS, COMMON_INGREDIENTS, PACKAGING_ITEMS, type IngredientTemplate, templateToIngredientCreate } from '../data/ingredientTemplates';
-export const InventorySetupStep: React.FC
= ({ onUpdate, onComplete, canContinue }) => {
+export const InventorySetupStep: React.FC = ({ onUpdate, onComplete, onPrevious, canContinue }) => {
const { t } = useTranslation();
// Get tenant ID
@@ -152,8 +152,8 @@ export const InventorySetupStep: React.FC = ({ onUpdate, onCompl
const handleEdit = (ingredient: any) => {
setFormData({
name: ingredient.name,
- category: ingredient.category || IngredientCategory.OTHER,
- unit_of_measure: ingredient.unit_of_measure,
+ category: (ingredient.category as IngredientCategory) || IngredientCategory.OTHER,
+ unit_of_measure: (ingredient.unit_of_measure as UnitOfMeasure) || UnitOfMeasure.UNITS,
brand: ingredient.brand || '',
standard_cost: ingredient.standard_cost?.toString() || '',
low_stock_threshold: ingredient.low_stock_threshold?.toString() || '',
@@ -205,8 +205,8 @@ export const InventorySetupStep: React.FC = ({ onUpdate, onCompl
const ingredientData = templateToIngredientCreate(template);
setFormData({
name: ingredientData.name,
- category: ingredientData.category,
- unit_of_measure: ingredientData.unit_of_measure,
+ category: (ingredientData.category as any) || IngredientCategory.OTHER,
+ unit_of_measure: (ingredientData.unit_of_measure as UnitOfMeasure) || UnitOfMeasure.UNITS,
brand: '',
standard_cost: ingredientData.standard_cost?.toString() || '',
low_stock_threshold: ingredientData.low_stock_threshold?.toString() || '',
@@ -551,7 +551,7 @@ export const InventorySetupStep: React.FC = ({ onUpdate, onCompl
- {t('setup_wizard:inventory.added_count', { count: ingredients.length, defaultValue: '{{count}} ingredient added' })}
+ {t('setup_wizard:inventory.added_count', { count: ingredients.length, defaultValue: '{count} ingredient added' })}
{ingredients.length >= 3 ? (
@@ -563,7 +563,7 @@ export const InventorySetupStep: React.FC = ({ onUpdate, onCompl
) : (
- {t('setup_wizard:inventory.need_more', 'Need {{count}} more', { count: 3 - ingredients.length })}
+ {t('setup_wizard:inventory.need_more', 'Need {count} more', { count: 3 - ingredients.length })}
)}
@@ -1007,16 +1007,29 @@ export const InventorySetupStep: React.FC
= ({ onUpdate, onCompl
)}
- {/* Continue button - only shown when used in onboarding context */}
- {onComplete && (
-
-
onComplete()}
- disabled={canContinue === false}
- className="px-6 py-3 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
- >
- {t('setup_wizard:navigation.continue', 'Continue →')}
-
+ {/* Navigation buttons */}
+ {!isAdding && onComplete && (
+
+
+
+ ← {t('common:previous', 'Anterior')}
+
+
+
+
+ onComplete()}
+ disabled={canContinue === false}
+ className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium flex items-center gap-2"
+ >
+ {t('common:next', 'Continuar →')}
+
+
)}
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 && (
-
-
onComplete()}
- disabled={canContinue === false}
- className="px-6 py-3 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
- >
- {t('setup_wizard:navigation.continue', 'Continue →')}
-
+ {/* Navigation buttons */}
+ {!isAdding && onComplete && (
+
+
+
+ ← {t('common:previous', 'Anterior')}
+
+
+
+
+ onComplete()}
+ disabled={canContinue === false}
+ className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium flex items-center gap-2"
+ >
+ {t('common:next', 'Continuar →')}
+
+
)}
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
handlePreviewTemplate(selectedTemplate?.id === template.id ? null : template)}
+ onClick={() => handlePreviewTemplate(selectedTemplate?.id === template.id ? (null as unknown as RecipeTemplate) : template)}
className="px-3 py-1.5 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)] rounded transition-colors"
>
{selectedTemplate?.id === template.id ? t('common:hide', 'Hide') : t('common:preview', 'Preview')}
@@ -447,7 +446,7 @@ export const RecipesSetupStep: React.FC = ({ onUpdate, onComplet
- {t('setup_wizard:recipes.added_count', { count: recipes.length, defaultValue: '{{count}} recipe added' })}
+ {t('setup_wizard:recipes.added_count', { count: recipes.length, defaultValue: '{count} recipe added' })}
{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) => (
@@ -614,6 +613,7 @@ export const RecipesSetupStep: React.FC = ({ onUpdate, onComplet
))}
+ {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 && (
+
+
+
+ ← {t('common:previous', 'Anterior')}
+
+
+
+
+ {!canContinue && recipes.length === 0 && (
+
+ {t('setup_wizard:recipes.add_minimum', 'Agrega al menos 1 receta para continuar')}
+
+ )}
+
onComplete?.()}
+ disabled={!canContinue}
+ className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium flex items-center gap-2"
+ >
+ {t('common:next', 'Continuar →')}
+
-
onComplete?.()}
- className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] transition-colors font-medium"
- >
- {t('common:next', 'Next')}
-
)}
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 && (
-
-
onComplete()}
- disabled={canContinue === false}
- className="px-6 py-3 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
- >
- {t('setup_wizard:navigation.continue', 'Continue →')}
-
+
+
+
+ ← {t('common:previous', 'Anterior')}
+
+
+
+
+ onComplete()}
+ disabled={canContinue === false}
+ className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium flex items-center gap-2"
+ >
+ {t('common:next', 'Completar Configuración ✓')}
+
+
)}
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 && (
- ← {t('common:previous', 'Previous')}
+ ← {t('common:previous', 'Anterior')}
)}
{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')}
)}
onComplete?.()}
disabled={!canContinue}
- className="px-6 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium flex items-center gap-2"
+ className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium flex items-center gap-2"
>
- {t('setup_wizard:navigation.continue', 'Continue →')}
+ {t('common:next', '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 && (
-
-
onComplete()}
- disabled={canContinue === false}
- className="px-6 py-3 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
- >
- {t('setup_wizard:navigation.continue', 'Continue →')}
-
+ {/* Navigation buttons */}
+ {!isAdding && onComplete && (
+
+
+
+ ← {t('common:previous', 'Anterior')}
+
+
+
+
+ onComplete()}
+ disabled={canContinue === false}
+ className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium flex items-center gap-2"
+ >
+ {t('common:next', 'Continuar →')}
+
+
)}
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"""