Imporve onboarding UI

This commit is contained in:
Urtzi Alfaro
2025-12-19 13:10:24 +01:00
parent 71ee2976a2
commit bfa5ff0637
39 changed files with 1016 additions and 483 deletions

View File

@@ -118,13 +118,16 @@ export const BakeryTypeSelectionStep: React.FC<BakeryTypeSelectionStepProps> = (
};
return (
<div className="max-w-6xl mx-auto p-4 md:p-6 space-y-6 md:space-y-8">
<div className="max-w-6xl mx-auto p-3 md:p-6 space-y-8 md:space-y-10">
{/* Header */}
<div className="text-center space-y-3 md:space-y-4">
<h1 className="text-2xl md:text-3xl font-bold text-[var(--text-primary)] px-2">
<div className="text-center space-y-4 md:space-y-5 animate-fade-in">
<div className="inline-block">
<div className="text-5xl md:text-6xl mb-3 animate-bounce-subtle">🏪</div>
</div>
<h1 className="text-3xl md:text-4xl font-bold text-[var(--text-primary)] px-2 leading-tight">
{t('onboarding:bakery_type.title', '¿Qué tipo de panadería tienes?')}
</h1>
<p className="text-base md:text-lg text-[var(--text-secondary)] max-w-2xl mx-auto px-4">
<p className="text-base md:text-lg text-[var(--text-secondary)] max-w-3xl mx-auto px-4 leading-relaxed">
{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<BakeryTypeSelectionStepProps> = (
</div>
{/* Bakery Type Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{bakeryTypes.map((type) => {
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 md:gap-6 animate-stagger-in">
{bakeryTypes.map((type, index) => {
const isSelected = selectedType === type.id;
const isHovered = hoveredType === type.id;
@@ -145,70 +148,79 @@ export const BakeryTypeSelectionStep: React.FC<BakeryTypeSelectionStepProps> = (
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 && (
<div className="absolute top-4 right-4 z-10">
<div className="w-8 h-8 bg-[var(--color-primary)] rounded-full flex items-center justify-center shadow-lg">
<Check className="w-5 h-5 text-white" strokeWidth={3} />
<div className="absolute top-4 right-4 z-10 animate-scale-in">
<div className="w-10 h-10 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary)]/80 rounded-full flex items-center justify-center shadow-xl animate-pulse-slow">
<Check className="w-6 h-6 text-white" strokeWidth={3} />
</div>
</div>
)}
{/* Accent Background */}
<div className={`absolute inset-0 bg-[var(--color-primary)]/5 transition-opacity ${isSelected ? 'opacity-100' : 'opacity-0'}`} />
{/* Accent Background with gradient */}
<div className={`absolute inset-0 bg-gradient-to-br from-[var(--color-primary)]/8 to-[var(--color-primary)]/3 transition-opacity duration-300 ${isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-50'}`} />
{/* Hover shimmer effect */}
<div className={`absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-full transition-transform duration-1000 ${isHovered ? 'translate-x-full' : ''}`} />
{/* Content */}
<div className="relative p-4 md:p-6 space-y-3 md:space-y-4">
<div className="relative p-5 md:p-7 space-y-4 md:space-y-5">
{/* Icon & Title */}
<div className="space-y-2 md:space-y-3">
<div className="text-4xl md:text-5xl">{type.icon}</div>
<h3 className="text-lg md:text-xl font-bold text-[var(--text-primary)]">
<div className="space-y-3 md:space-y-4">
<div className="text-5xl md:text-6xl transform transition-transform duration-300 group-hover:scale-110">
{type.icon}
</div>
<h3 className="text-xl md:text-2xl font-bold text-[var(--text-primary)] leading-tight">
{type.name}
</h3>
<p className="text-sm text-[var(--text-secondary)] leading-relaxed">
<p className="text-sm md:text-base text-[var(--text-secondary)] leading-relaxed">
{type.description}
</p>
</div>
{/* Features */}
<div className="space-y-2 pt-2">
<h4 className="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wide">
<div className="space-y-3 pt-3">
<h4 className="text-xs font-bold text-[var(--text-secondary)] uppercase tracking-wider flex items-center gap-2">
<span className="w-1 h-4 bg-[var(--color-primary)] rounded-full"></span>
{t('onboarding:bakery_type.features_label', 'Características')}
</h4>
<ul className="space-y-1.5">
{type.features.map((feature, index) => (
<ul className="space-y-2.5">
{type.features.map((feature, featureIndex) => (
<li
key={index}
className="text-sm text-[var(--text-primary)] flex items-start gap-2"
key={featureIndex}
className="text-sm md:text-base text-[var(--text-primary)] flex items-start gap-2.5 group/feature"
>
<span className="text-[var(--color-primary)] mt-0.5 flex-shrink-0"></span>
<span>{feature}</span>
<span className="text-[var(--color-primary)] mt-0.5 flex-shrink-0 font-bold text-lg group-hover/feature:scale-125 transition-transform"></span>
<span className="leading-snug">{feature}</span>
</li>
))}
</ul>
</div>
{/* Examples */}
<div className="space-y-2 pt-2 border-t border-[var(--border-color)]">
<h4 className="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wide">
<div className="space-y-3 pt-4 border-t-2 border-[var(--border-color)]/50">
<h4 className="text-xs font-bold text-[var(--text-secondary)] uppercase tracking-wider flex items-center gap-2">
<span className="w-1 h-4 bg-[var(--color-primary)] rounded-full"></span>
{t('onboarding:bakery_type.examples_label', 'Ejemplos')}
</h4>
<div className="flex flex-wrap gap-2">
{type.examples.map((example, index) => (
{type.examples.map((example, exampleIndex) => (
<span
key={index}
className="text-xs px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-full text-[var(--text-secondary)]"
key={exampleIndex}
className="text-xs md:text-sm px-3 py-1.5 bg-[var(--bg-primary)] border-2 border-[var(--border-color)] rounded-full text-[var(--text-secondary)] hover:border-[var(--color-primary)] hover:text-[var(--color-primary)] transition-all duration-200 cursor-default"
>
{example}
</span>
@@ -223,16 +235,17 @@ export const BakeryTypeSelectionStep: React.FC<BakeryTypeSelectionStepProps> = (
{/* Additional Info */}
{selectedType && (
<div className="bg-[var(--color-primary)]/10 border border-[var(--color-primary)]/20 rounded-lg p-4 md:p-6">
<div className="flex items-start gap-3">
<div className="text-2xl md:text-3xl flex-shrink-0">
<div className="bg-gradient-to-br from-[var(--color-primary)]/10 to-[var(--color-primary)]/5 border-2 border-[var(--color-primary)]/30 rounded-2xl p-5 md:p-7 animate-slide-up shadow-lg">
<div className="flex items-start gap-4 md:gap-5">
<div className="text-4xl md:text-5xl flex-shrink-0 animate-bounce-subtle">
{bakeryTypes.find(t => t.id === selectedType)?.icon}
</div>
<div className="space-y-2">
<h4 className="font-semibold text-[var(--text-primary)]">
<div className="space-y-3 flex-1">
<h4 className="text-lg md:text-xl font-bold text-[var(--text-primary)] flex items-center gap-2">
<span className="text-2xl"></span>
{t('onboarding:bakery_type.selected_info_title', 'Perfecto para tu panadería')}
</h4>
<p className="text-sm text-[var(--text-secondary)]">
<p className="text-sm md:text-base text-[var(--text-secondary)] leading-relaxed">
{selectedType === 'production' &&
t(
'onboarding:bakery_type.production.selected_info',
@@ -255,22 +268,27 @@ export const BakeryTypeSelectionStep: React.FC<BakeryTypeSelectionStepProps> = (
)}
{/* Help Text & Continue Button */}
<div className="text-center space-y-4">
<p className="text-sm text-[var(--text-secondary)]">
{t(
'onboarding:bakery_type.help_text',
'💡 No te preocupes, siempre puedes cambiar esto más tarde en la configuración'
)}
</p>
<div className="text-center space-y-5 pt-2">
<div className="inline-flex items-center gap-2 px-4 py-2 bg-[var(--bg-secondary)] rounded-full border border-[var(--border-color)]">
<span className="text-xl">💡</span>
<p className="text-sm md:text-base text-[var(--text-secondary)]">
{t(
'onboarding:bakery_type.help_text',
'No te preocupes, siempre puedes cambiar esto más tarde en la configuración'
)}
</p>
</div>
<div className="flex justify-center pt-2">
<Button
onClick={handleContinue}
disabled={!selectedType}
size="lg"
className="w-full sm:w-auto sm:min-w-[200px]"
className={`w-full sm:w-auto sm:min-w-[250px] text-base md:text-lg font-semibold transform transition-all duration-300 ${
selectedType ? 'hover:scale-105 shadow-lg' : ''
}`}
>
{t('onboarding:bakery_type.continue_button', 'Continuar')}
{t('onboarding:bakery_type.continue_button', 'Continuar')}
</Button>
</div>
</div>

View File

@@ -20,54 +20,59 @@ export const CompletionStep: React.FC<CompletionStepProps> = ({
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 (
<div className="text-center space-y-8 max-w-5xl mx-auto">
<div className="text-center space-y-10 max-w-5xl mx-auto px-4 animate-fade-in">
{/* Confetti effect */}
<div className="absolute inset-0 pointer-events-none overflow-hidden">
<div className="absolute top-0 left-1/4 text-4xl animate-bounce-subtle" style={{ animationDelay: '0s' }}>🎉</div>
<div className="absolute top-10 right-1/4 text-3xl animate-bounce-subtle" style={{ animationDelay: '0.5s' }}></div>
<div className="absolute top-5 left-1/2 text-4xl animate-bounce-subtle" style={{ animationDelay: '1s' }}>🎊</div>
</div>
{/* Animated Success Icon */}
<div className="relative mx-auto w-32 h-32">
<div className="absolute inset-0 bg-[var(--color-success)]/20 rounded-full animate-ping"></div>
<div className="relative w-32 h-32 bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success)]/70 rounded-full flex items-center justify-center shadow-lg">
<CheckCircle2 className="w-16 h-16 text-white" />
<div className="relative mx-auto w-40 h-40 animate-scale-in">
<div className="absolute inset-0 bg-[var(--color-success)]/30 rounded-full animate-ping" style={{ animationDuration: '2s' }}></div>
<div className="absolute inset-2 bg-[var(--color-success)]/20 rounded-full animate-pulse"></div>
<div className="relative w-40 h-40 bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success)]/80 rounded-full flex items-center justify-center shadow-2xl transform transition-transform hover:scale-110">
<CheckCircle2 className="w-20 h-20 text-white animate-pulse-slow" />
</div>
</div>
{/* Success Message */}
<div className="space-y-4">
<h1 className="text-4xl font-bold bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-success)] bg-clip-text text-transparent">
<div className="space-y-5 animate-slide-up">
<h1 className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-[var(--color-primary)] via-[var(--color-success)] to-[var(--color-primary)] bg-clip-text text-transparent animate-shimmer" style={{ backgroundSize: '200% auto' }}>
{t('onboarding:completion.congratulations', '¡Felicidades! Tu Sistema Está Listo')}
</h1>
<p className="text-xl text-[var(--text-secondary)] max-w-2xl mx-auto">
<p className="text-lg md:text-xl text-[var(--text-secondary)] max-w-3xl mx-auto leading-relaxed">
{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 })}
</p>
</div>
{/* What You Configured */}
<div className="bg-[var(--bg-secondary)] rounded-lg p-6 max-w-3xl mx-auto text-left">
<h3 className="font-semibold text-lg mb-4 text-center text-[var(--text-primary)]">
<div className="bg-gradient-to-br from-[var(--bg-secondary)] to-[var(--bg-primary)] rounded-2xl p-7 md:p-8 max-w-3xl mx-auto text-left border-2 border-[var(--border-color)]/50 shadow-xl">
<h3 className="font-bold text-xl md:text-2xl mb-6 text-center text-[var(--text-primary)] flex items-center justify-center gap-3">
<span className="text-2xl">📋</span>
{t('onboarding:completion.what_configured', 'Lo Que Has Configurado')}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="flex items-start gap-3">
<div className="w-6 h-6 bg-[var(--color-success)]/10 rounded flex items-center justify-center flex-shrink-0 mt-0.5">
<svg className="w-4 h-4 text-[var(--color-success)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -155,68 +160,68 @@ export const CompletionStep: React.FC<CompletionStepProps> = ({
</div>
{/* Quick Access Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 max-w-4xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5 max-w-5xl mx-auto">
<button
onClick={() => 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"
>
<BarChart className="w-8 h-8 text-[var(--color-primary)] mb-2 group-hover:scale-110 transition-transform" />
<h4 className="font-semibold text-[var(--text-primary)] mb-1">
<BarChart className="w-10 h-10 md:w-12 md:h-12 text-[var(--color-primary)] mb-3 group-hover:scale-125 transition-transform duration-300" />
<h4 className="font-bold text-base md:text-lg text-[var(--text-primary)] mb-1.5">
{t('onboarding:completion.quick.analytics', 'Analíticas')}
</h4>
<p className="text-xs text-[var(--text-secondary)]">
<p className="text-xs md:text-sm text-[var(--text-secondary)]">
{t('onboarding:completion.quick.analytics_desc', 'Ver predicciones y métricas')}
</p>
</button>
<button
onClick={() => 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"
>
<ShoppingCart className="w-8 h-8 text-[var(--color-success)] mb-2 group-hover:scale-110 transition-transform" />
<h4 className="font-semibold text-[var(--text-primary)] mb-1">
<ShoppingCart className="w-10 h-10 md:w-12 md:h-12 text-[var(--color-success)] mb-3 group-hover:scale-125 transition-transform duration-300" />
<h4 className="font-bold text-base md:text-lg text-[var(--text-primary)] mb-1.5">
{t('onboarding:completion.quick.inventory', 'Inventario')}
</h4>
<p className="text-xs text-[var(--text-secondary)]">
<p className="text-xs md:text-sm text-[var(--text-secondary)]">
{t('onboarding:completion.quick.inventory_desc', 'Gestionar stock y productos')}
</p>
</button>
<button
onClick={() => 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"
>
<Users className="w-8 h-8 text-[var(--color-info)] mb-2 group-hover:scale-110 transition-transform" />
<h4 className="font-semibold text-[var(--text-primary)] mb-1">
<Users className="w-10 h-10 md:w-12 md:h-12 text-[var(--color-info)] mb-3 group-hover:scale-125 transition-transform duration-300" />
<h4 className="font-bold text-base md:text-lg text-[var(--text-primary)] mb-1.5">
{t('onboarding:completion.quick.procurement', 'Compras')}
</h4>
<p className="text-xs text-[var(--text-secondary)]">
<p className="text-xs md:text-sm text-[var(--text-secondary)]">
{t('onboarding:completion.quick.procurement_desc', 'Gestionar pedidos')}
</p>
</button>
<button
onClick={() => 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"
>
<TrendingUp className="w-8 h-8 text-[var(--color-warning)] mb-2 group-hover:scale-110 transition-transform" />
<h4 className="font-semibold text-[var(--text-primary)] mb-1">
<TrendingUp className="w-10 h-10 md:w-12 md:h-12 text-[var(--color-warning)] mb-3 group-hover:scale-125 transition-transform duration-300" />
<h4 className="font-bold text-base md:text-lg text-[var(--text-primary)] mb-1.5">
{t('onboarding:completion.quick.production', 'Producción')}
</h4>
<p className="text-xs text-[var(--text-secondary)]">
<p className="text-xs md:text-sm text-[var(--text-secondary)]">
{t('onboarding:completion.quick.production_desc', 'Planificar producción')}
</p>
</button>
</div>
{/* Tips for Success */}
<div className="bg-gradient-to-r from-[var(--color-primary)]/10 to-[var(--color-info)]/10 border border-[var(--color-primary)]/20 rounded-xl p-6 max-w-3xl mx-auto text-left">
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-info)] text-white rounded-full flex items-center justify-center flex-shrink-0">
<Zap className="w-6 h-6" />
<div className="bg-gradient-to-r from-[var(--color-primary)]/10 via-[var(--color-success)]/10 to-[var(--color-info)]/10 border-2 border-[var(--color-primary)]/30 rounded-2xl p-7 md:p-8 max-w-3xl mx-auto text-left shadow-lg">
<div className="flex items-start gap-5">
<div className="w-14 h-14 md:w-16 md:h-16 bg-gradient-to-br from-[var(--color-primary)] via-[var(--color-success)] to-[var(--color-info)] text-white rounded-2xl flex items-center justify-center flex-shrink-0 shadow-lg animate-pulse-slow">
<Zap className="w-7 h-7 md:w-8 md:h-8" />
</div>
<div className="flex-1">
<h3 className="font-bold text-lg mb-3 text-[var(--text-primary)]">
<h3 className="font-bold text-xl md:text-2xl mb-4 text-[var(--text-primary)]">
{t('onboarding:completion.tips_title', 'Consejos para Maximizar tu Éxito')}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
@@ -250,13 +255,13 @@ export const CompletionStep: React.FC<CompletionStepProps> = ({
</div>
{/* Primary Action Button */}
<div className="flex justify-center items-center pt-4">
<div className="flex justify-center items-center pt-6">
<Button
onClick={handleExploreDashboard}
size="lg"
className="px-12 py-4 text-lg font-semibold shadow-lg hover:shadow-xl transition-all"
className="px-16 py-5 text-lg md:text-xl font-bold shadow-2xl hover:shadow-3xl transition-all duration-300 transform hover:scale-110 hover:-translate-y-1 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-success)]"
>
{t('onboarding:completion.go_to_dashboard', 'Comenzar a Usar el Sistema')}
{t('onboarding:completion.go_to_dashboard', 'Comenzar a Usar el Sistema')} 🚀
</Button>
</div>

View File

@@ -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<InitialStockEntryStepProps> = ({
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const addStockMutation = useAddStock();
const { data: stockData } = useStock(tenantId);
const [isSaving, setIsSaving] = useState(false);
const [products, setProducts] = useState<ProductWithStock[]>(() => {
@@ -54,6 +55,30 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
}));
});
// 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<InitialStockEntryStepProps> = ({
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);
}

View File

@@ -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<InventoryReviewStepProps> = ({
// 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<InventoryReviewStepProps> = ({
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<InventoryReviewStepProps> = ({
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<InventoryReviewStepProps> = ({
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<InventoryReviewStepProps> = ({
<div className="flex gap-2 border-b border-[var(--border-color)] overflow-x-auto scrollbar-hide -mx-2 px-2">
<button
onClick={() => 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})
</button>
<button
onClick={() => 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)]'
}`}
>
<ShoppingBag className="w-5 h-5" />
<span className="hidden sm:inline">{t('inventory:filter.finished_products', 'Productos Terminados')}</span>
@@ -461,11 +506,10 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
</button>
<button
onClick={() => 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)]'
}`}
>
<Package className="w-5 h-5" />
{t('inventory:filter.ingredients', 'Ingredientes')} ({counts.ingredients})
@@ -492,11 +536,10 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
<h5 className="font-medium text-[var(--text-primary)] truncate">{item.name}</h5>
{/* Product Type Badge */}
<span className={`text-xs px-2 py-0.5 rounded-full ${
item.product_type === ProductType.FINISHED_PRODUCT
? 'bg-green-100 text-green-800'
: 'bg-blue-100 text-blue-800'
}`}>
<span className={`text-xs px-2 py-0.5 rounded-full ${item.product_type === ProductType.FINISHED_PRODUCT
? 'bg-green-100 text-green-800'
: 'bg-blue-100 text-blue-800'
}`}>
{item.product_type === ProductType.FINISHED_PRODUCT ? (
<span className="flex items-center gap-1">
<ShoppingBag className="w-3 h-3" />
@@ -579,11 +622,10 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
<button
type="button"
onClick={() => 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 && (
<CheckCircle2 className="absolute top-2 right-2 w-5 h-5 text-[var(--color-primary)]" />
@@ -600,11 +642,10 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
<button
type="button"
onClick={() => 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 && (
<CheckCircle2 className="absolute top-2 right-2 w-5 h-5 text-[var(--color-primary)]" />
@@ -724,11 +765,10 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
<button
type="button"
onClick={() => 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 && (
<CheckCircle2 className="absolute top-2 right-2 w-5 h-5 text-[var(--color-primary)]" />
@@ -745,11 +785,10 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
<button
type="button"
onClick={() => 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 && (
<CheckCircle2 className="absolute top-2 right-2 w-5 h-5 text-[var(--color-primary)]" />
@@ -862,9 +901,9 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
{isSubmitting
? t('common:saving', 'Guardando...')
: <>
<span className="hidden md:inline">{t('common:continue', 'Continuar')} ({inventoryItems.length} {t('common:items', 'productos')}) </span>
<span className="md:hidden">{t('common:continue', 'Continuar')} ({inventoryItems.length}) </span>
</>
<span className="hidden md:inline">{t('common:continue', 'Continuar')} ({inventoryItems.length} {t('common:items', 'productos')}) </span>
<span className="md:hidden">{t('common:continue', 'Continuar')} ({inventoryItems.length}) </span>
</>
}
</Button>
</div>

View File

@@ -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<RegisterTenantStepProps> = ({
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<RegisterTenantStepProps> = ({
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<RegisterTenantStepProps> = ({
const [errors, setErrors] = useState<Record<string, string>>({});
const registerBakery = useRegisterBakery();
const updateTenant = useUpdateTenant();
const handleInputChange = (field: keyof BakeryRegistration, value: string) => {
setFormData(prev => ({
@@ -143,14 +168,31 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
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<RegisterTenantStepProps> = ({
};
return (
<div className="space-y-4 md:space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6">
<Input
label={isEnterprise ? "Nombre del Obrador Central" : "Nombre de la Panadería"}
placeholder={isEnterprise ? "Ingresa el nombre de tu obrador central" : "Ingresa el nombre de tu panadería"}
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
error={errors.name}
isRequired
/>
<div className="space-y-6 md:space-y-8">
{/* Informational header */}
<div className="bg-gradient-to-r from-[var(--color-primary)]/10 to-[var(--color-primary)]/5 border-l-4 border-[var(--color-primary)] rounded-lg p-4 md:p-5">
<div className="flex items-start gap-3">
<div className="text-2xl flex-shrink-0">{isEnterprise ? '🏭' : '🏪'}</div>
<div>
<h3 className="font-semibold text-[var(--text-primary)] mb-1">
{isEnterprise ? 'Registra tu Obrador Central' : 'Registra tu Panadería'}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{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.'}
</p>
</div>
</div>
</div>
<Input
label="Teléfono"
type="tel"
placeholder="+34 123 456 789"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
error={errors.phone}
isRequired
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 md:gap-6">
<div className="transform transition-all duration-200 hover:scale-[1.01]">
<Input
label={isEnterprise ? "Nombre del Obrador Central" : "Nombre de la Panadería"}
placeholder={isEnterprise ? "Ingresa el nombre de tu obrador central" : "Ingresa el nombre de tu panadería"}
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
error={errors.name}
isRequired
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
<div className="transform transition-all duration-200 hover:scale-[1.01]">
<Input
label="Teléfono"
type="tel"
placeholder="+34 123 456 789"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
error={errors.phone}
isRequired
/>
</div>
<div className="md:col-span-2 transform transition-all duration-200 hover:scale-[1.01] relative z-20">
<label className="block text-sm font-semibold text-[var(--text-primary)] mb-2 flex items-center gap-2">
<span className="text-lg">📍</span>
Dirección <span className="text-red-500">*</span>
</label>
<AddressAutocomplete
@@ -240,45 +304,60 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
required
/>
{errors.address && (
<div className="mt-1 text-sm text-red-600">
<div className="mt-2 text-sm text-red-600 flex items-center gap-1.5 animate-shake">
<span></span>
{errors.address}
</div>
)}
</div>
<Input
label="Código Postal"
placeholder="28001"
value={formData.postal_code}
onChange={(e) => handleInputChange('postal_code', e.target.value)}
error={errors.postal_code}
isRequired
maxLength={5}
/>
<div className="transform transition-all duration-200 hover:scale-[1.01]">
<Input
label="Código Postal"
placeholder="28001"
value={formData.postal_code}
onChange={(e) => handleInputChange('postal_code', e.target.value)}
error={errors.postal_code}
isRequired
maxLength={5}
/>
</div>
<Input
label="Ciudad (Opcional)"
placeholder="Madrid"
value={formData.city}
onChange={(e) => handleInputChange('city', e.target.value)}
error={errors.city}
/>
<div className="transform transition-all duration-200 hover:scale-[1.01]">
<Input
label="Ciudad (Opcional)"
placeholder="Madrid"
value={formData.city}
onChange={(e) => handleInputChange('city', e.target.value)}
error={errors.city}
/>
</div>
</div>
{errors.submit && (
<div className="text-[var(--color-error)] text-sm bg-[var(--color-error)]/10 p-3 rounded-lg">
{errors.submit}
<div className="bg-gradient-to-r from-[var(--color-error)]/10 to-[var(--color-error)]/5 border-l-4 border-[var(--color-error)] rounded-lg p-4 animate-shake">
<div className="flex items-start gap-3">
<span className="text-2xl flex-shrink-0"></span>
<div>
<h4 className="font-semibold text-[var(--color-error)] mb-1">Error al registrar</h4>
<p className="text-sm text-[var(--text-secondary)]">{errors.submit}</p>
</div>
</div>
</div>
)}
<div className="flex justify-end">
<div className="flex justify-end pt-4 border-t-2 border-[var(--border-color)]/50">
<Button
onClick={handleSubmit}
isLoading={registerBakery.isPending}
loadingText={isEnterprise ? "Registrando obrador..." : "Registrando..."}
isLoading={registerBakery.isPending || updateTenant.isPending}
loadingText={tenantId ? "Actualizando obrador..." : (isEnterprise ? "Registrando obrador..." : "Registrando...")}
size="lg"
className="w-full sm:w-auto sm:min-w-[280px] text-base font-semibold transform transition-all duration-300 hover:scale-105 shadow-lg"
>
{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 →")
}
</Button>
</div>
</div>