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

@@ -125,7 +125,7 @@ export function CollapsibleSetupBanner({ remainingSections, progressPercentage,
{/* Text */}
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-base mb-1" style={{ color: 'var(--text-primary)' }}>
📋 {t('dashboard:setup_banner.title', '{{count}} paso(s) más para desbloquear todas las funciones', { count: remainingSections.length })}
📋 {t('dashboard:setup_banner.title', '{count} paso(s) más para desbloquear todas las funciones', { count: remainingSections.length })}
</h3>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{remainingSections.map(s => s.title).join(', ')} {t('dashboard:setup_banner.recommended', '(recomendado)')}

View File

@@ -344,6 +344,17 @@ const OnboardingWizardContent: React.FC = () => {
return;
}
// CRITICAL: Guard against undefined currentStep
if (!currentStep) {
console.error('❌ Current step is undefined!', {
currentStepIndex,
visibleStepsLength: VISIBLE_STEPS.length,
visibleSteps: VISIBLE_STEPS.map(s => s.id),
wizardState: wizardContext.state,
});
return;
}
if (markStepCompleted.isPending) {
console.warn(`⚠️ Step completion already in progress for "${currentStep.id}", skipping duplicate call`);
return;
@@ -449,7 +460,7 @@ const OnboardingWizardContent: React.FC = () => {
if (response.failed_count > 0) {
console.warn('⚠️ Some child tenants failed to create:', response.failed_tenants);
// Show specific errors for each failed tenant
response.failed_tenants.forEach(failed => {
console.error(`Failed to create tenant ${failed.name} (${failed.location_code}):`, failed.error);
@@ -492,8 +503,32 @@ const OnboardingWizardContent: React.FC = () => {
}
if (currentStep.id === 'completion') {
wizardContext.resetWizard();
navigate(isNewTenant ? '/app/dashboard' : '/app');
// CRITICAL: Call backend to mark onboarding as fully completed
if (data?.markFullyCompleted) {
console.log('🎯 [Completion] Marking onboarding as fully completed in backend...');
try {
const { onboardingService } = await import('../../../api');
await onboardingService.completeOnboarding();
console.log('✅ [Completion] Onboarding marked as fully completed successfully');
} catch (error: any) {
console.error('❌ [Completion] Failed to mark onboarding as fully completed:', error);
// If completion fails due to conditional steps, it's okay to continue
// The backend error is logged but we don't block the user
if (error?.message?.includes('incomplete steps')) {
console.warn('⚠️ [Completion] Some conditional steps incomplete - this is expected for certain tiers');
}
}
}
// DON'T reset wizard context here - it breaks VISIBLE_STEPS calculation
// and causes "can't access property 'component', i is undefined" error
// The context will be reset when user returns to onboarding flow
// Don't navigate here - let the CompletionStep handle navigation
// This prevents double navigation which can cause issues
if (!data?.redirectTo) {
navigate(isNewTenant ? '/app/dashboard' : '/app');
}
} else {
if (currentStepIndex < VISIBLE_STEPS.length - 1) {
setCurrentStepIndex(currentStepIndex + 1);
@@ -583,69 +618,141 @@ const OnboardingWizardContent: React.FC = () => {
);
}
// CRITICAL: Guard against undefined currentStep in render
if (!currentStep) {
console.error('❌ Cannot render: currentStep is undefined!', {
currentStepIndex,
visibleStepsLength: VISIBLE_STEPS.length,
visibleSteps: VISIBLE_STEPS.map(s => s.id),
wizardState: wizardContext.state,
});
return (
<div className="max-w-4xl mx-auto px-4 sm:px-6">
<Card padding="lg" shadow="lg">
<CardBody>
<div className="text-center space-y-4">
<div className="w-14 h-14 sm:w-16 sm:h-16 mx-auto bg-[var(--color-error)]/10 rounded-full flex items-center justify-center">
<svg className="w-7 h-7 sm:w-8 sm:h-8 text-[var(--color-error)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<h2 className="text-base sm:text-lg font-semibold text-[var(--text-primary)] mb-2">
{t('onboarding:errors.step_error', 'Error de configuración')}
</h2>
<p className="text-sm sm:text-base text-[var(--text-secondary)] mb-4 px-2">
{t('onboarding:errors.step_missing', 'No se pudo cargar el paso actual. Por favor, recarga la página.')}
</p>
<Button onClick={() => window.location.reload()} variant="primary" size="lg">
{t('common:reload', 'Recargar página')}
</Button>
</div>
</div>
</CardBody>
</Card>
</div>
);
}
const StepComponent = currentStep.component;
const progressPercentage = isNewTenant
? ((currentStepIndex + 1) / VISIBLE_STEPS.length) * 100
: userProgress?.completion_percentage || ((currentStepIndex + 1) / VISIBLE_STEPS.length) * 100;
return (
<div className="max-w-4xl mx-auto px-2 sm:px-4 md:px-6 space-y-3 sm:space-y-4 md:space-y-6 pb-4 md:pb-6">
<div className="max-w-4xl mx-auto px-2 sm:px-4 md:px-6 space-y-4 sm:space-y-5 md:space-y-6 pb-6 md:pb-8">
{/* Progress Header */}
<Card shadow="sm" padding="md">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-3 sm:mb-4 space-y-2 sm:space-y-0">
<Card shadow="md" padding="lg" className="border border-[var(--border-color)]/50">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-4 sm:mb-5 space-y-3 sm:space-y-0">
<div className="text-center sm:text-left">
<h1 className="text-lg sm:text-xl md:text-2xl font-bold text-[var(--text-primary)]">
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-[var(--text-primary)]">
{isNewTenant ? t('onboarding:wizard.title_new', 'Nueva Panadería') : t('onboarding:wizard.title', 'Configuración Inicial')}
</h1>
<p className="text-[var(--text-secondary)] text-xs sm:text-sm mt-1">
<p className="text-[var(--text-secondary)] text-sm sm:text-base mt-1.5">
{t('onboarding:wizard.subtitle', 'Configura tu sistema paso a paso')}
</p>
</div>
<div className="text-center sm:text-right">
<div className="text-sm text-[var(--text-secondary)]">
<div className="text-center sm:text-right bg-[var(--bg-secondary)] rounded-lg px-4 py-3 sm:px-5 sm:py-3.5">
<div className="text-base sm:text-lg font-semibold text-[var(--text-primary)]">
{t('onboarding:wizard.progress.step_of', 'Paso {current} de {total}', {
current: currentStepIndex + 1,
total: VISIBLE_STEPS.length
})}
</div>
<div className="text-xs text-[var(--text-tertiary)]">
<div className="text-sm text-[var(--text-tertiary)] mt-0.5">
{Math.round(progressPercentage)}% {t('onboarding:wizard.progress.completed', 'completado')}
</div>
</div>
</div>
{/* Progress Bar */}
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-2 sm:h-3">
<div
className="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary)]/80 h-2 sm:h-3 rounded-full transition-all duration-500 ease-out"
style={{ width: `${progressPercentage}%` }}
/>
{/* Enhanced Progress Bar with milestone markers */}
<div className="relative">
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-3 sm:h-4 shadow-inner">
<div
className="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary)]/90 h-3 sm:h-4 rounded-full transition-all duration-700 ease-out shadow-sm relative overflow-hidden"
style={{ width: `${progressPercentage}%` }}
>
{/* Animated shimmer effect */}
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-shimmer"
style={{
backgroundSize: '200% 100%',
animation: 'shimmer 2s infinite'
}}
/>
</div>
</div>
{/* Step milestone indicators */}
<div className="absolute top-0 left-0 right-0 flex justify-between" style={{ marginTop: '-2px' }}>
{VISIBLE_STEPS.map((_, index) => {
const stepProgress = ((index + 1) / VISIBLE_STEPS.length) * 100;
const isCompleted = stepProgress <= progressPercentage;
const isCurrent = index === currentStepIndex;
return (
<div
key={index}
className={`w-2 h-2 sm:w-3 sm:h-3 rounded-full transition-all duration-300 ${isCompleted
? 'bg-[var(--color-primary)] ring-2 ring-white shadow-md scale-110'
: isCurrent
? 'bg-[var(--color-primary)]/50 ring-2 ring-[var(--color-primary)]/30 animate-pulse'
: 'bg-[var(--bg-secondary)] border-2 border-[var(--border-color)]'
}`}
style={{ marginLeft: index === 0 ? '0' : undefined, marginRight: index === VISIBLE_STEPS.length - 1 ? '0' : undefined }}
/>
);
})}
</div>
</div>
</Card>
{/* Step Content */}
<Card shadow="lg" padding="none">
<CardHeader padding="md" divider>
<div className="flex items-center space-x-2 sm:space-x-3">
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center flex-shrink-0">
<div className="w-5 h-5 sm:w-6 sm:h-6 bg-[var(--color-primary)] rounded-full flex items-center justify-center text-white text-xs font-bold">
{currentStepIndex + 1}
<Card shadow="xl" padding="none" className="border border-[var(--border-color)]/30 transition-all duration-300">
<CardHeader padding="lg" divider className="bg-gradient-to-r from-[var(--bg-primary)] to-[var(--bg-secondary)]">
<div className="flex items-center space-x-3 sm:space-x-4">
<div className="relative flex-shrink-0">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary)]/80 rounded-2xl flex items-center justify-center shadow-lg transform transition-transform duration-300 hover:scale-105">
<div className="text-white text-lg sm:text-xl font-bold">
{currentStepIndex + 1}
</div>
</div>
{/* Decorative ring */}
<div className="absolute inset-0 rounded-2xl bg-[var(--color-primary)]/20 animate-ping" style={{ animationDuration: '3s' }} />
</div>
<div className="flex-1">
<h2 className="text-lg sm:text-xl font-semibold text-[var(--text-primary)]">
<div className="flex-1 min-w-0">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--text-primary)] mb-1">
{currentStep.title}
</h2>
<p className="text-[var(--text-secondary)] text-xs sm:text-sm">
<p className="text-[var(--text-secondary)] text-sm sm:text-base">
{currentStep.description}
</p>
</div>
</div>
</CardHeader>
<CardBody padding="md">
<CardBody padding="lg" className="bg-[var(--bg-primary)]">
<StepComponent
onNext={() => {}}
onNext={() => { }}
onPrevious={handleGoToPrevious}
onComplete={handleStepComplete}
onUpdate={handleStepUpdate}
@@ -656,15 +763,15 @@ const OnboardingWizardContent: React.FC = () => {
// Pass AI data and file to InventoryReviewStep
currentStep.id === 'inventory-review'
? {
uploadedFile: wizardContext.state.uploadedFile,
validationResult: wizardContext.state.uploadedFileValidation,
aiSuggestions: wizardContext.state.aiSuggestions,
uploadedFileName: wizardContext.state.uploadedFileName || '',
uploadedFileSize: wizardContext.state.uploadedFileSize || 0,
}
uploadedFile: wizardContext.state.uploadedFile,
validationResult: wizardContext.state.uploadedFileValidation,
aiSuggestions: wizardContext.state.aiSuggestions,
uploadedFileName: wizardContext.state.uploadedFileName || '',
uploadedFileSize: wizardContext.state.uploadedFileSize || 0,
}
: // Pass inventory items to InitialStockEntryStep
currentStep.id === 'initial-stock-entry' && wizardContext.state.inventoryItems
? {
? {
productsWithStock: wizardContext.state.inventoryItems.map(item => ({
id: item.id,
name: item.name,
@@ -674,7 +781,7 @@ const OnboardingWizardContent: React.FC = () => {
initialStock: undefined,
}))
}
: undefined
: undefined
}
/>
</CardBody>

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>

View File

@@ -113,7 +113,7 @@ export const StepNavigation: React.FC<StepNavigationProps> = ({
{!canContinue && !currentStep.isOptional && !isLastStep && (
<div className="w-full text-center sm:text-right text-xs text-[var(--text-tertiary)] mt-2 sm:mt-0 sm:absolute sm:bottom-2 sm:right-6">
{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)
})

View File

@@ -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<SetupStepProps> = ({ onUpdate, onComplete, canContinue }) => {
export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplete, onPrevious, canContinue }) => {
const { t } = useTranslation();
// Get tenant ID
@@ -152,8 +152,8 @@ export const InventorySetupStep: React.FC<SetupStepProps> = ({ 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<SetupStepProps> = ({ 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<SetupStepProps> = ({ onUpdate, onCompl
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
<span className="text-sm font-medium text-[var(--text-primary)]">
{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' })}
</span>
</div>
{ingredients.length >= 3 ? (
@@ -563,7 +563,7 @@ export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onCompl
</div>
) : (
<div className="text-xs text-[var(--text-secondary)]">
{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 })}
</div>
)}
</div>
@@ -1007,16 +1007,29 @@ export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onCompl
</div>
)}
{/* Continue button - only shown when used in onboarding context */}
{onComplete && (
<div className="flex justify-end mt-6 pt-6 border-t border-[var(--border-secondary)]">
<button
onClick={() => 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 →')}
</button>
{/* Navigation buttons */}
{!isAdding && onComplete && (
<div className="flex items-center justify-between gap-4 pt-6 mt-6 border-t border-[var(--border-secondary)]">
<div className="flex gap-2">
<button
type="button"
onClick={onPrevious}
className="px-4 py-2 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] rounded-lg transition-colors font-medium"
>
{t('common:previous', 'Anterior')}
</button>
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => 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 →')}
</button>
</div>
</div>
)}
</div>

View File

@@ -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<SetupStepProps> = ({ onUpdate, onComplete, canContinue }) => {
export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplete, onPrevious, canContinue }) => {
const { t } = useTranslation();
// Get tenant ID and user
@@ -75,14 +74,14 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ 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<SetupStepProps> = ({ onUpdate, onComplet
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-sm font-medium text-[var(--text-primary)]">
{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' })}
</span>
</div>
{templates.length >= 2 ? (
@@ -252,7 +251,7 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ 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 && <p className="mt-1 text-xs text-[var(--color-error)]">{errors.name}</p>}
@@ -274,11 +273,10 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ 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)]'
} `}
>
<div className="text-lg mb-1">{option.icon}</div>
<div className="text-xs font-medium text-[var(--text-primary)]">{option.label}</div>
@@ -325,11 +323,10 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ 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}
</button>
@@ -429,16 +426,29 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
</div>
)}
{/* Continue button - only shown when used in onboarding context */}
{onComplete && (
<div className="flex justify-end mt-6 pt-6 border-t border-[var(--border-secondary)]">
<button
onClick={() => 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 →')}
</button>
{/* Navigation buttons */}
{!isAdding && onComplete && (
<div className="flex items-center justify-between gap-4 pt-6 mt-6 border-t border-[var(--border-secondary)]">
<div className="flex gap-2">
<button
type="button"
onClick={onPrevious}
className="px-4 py-2 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] rounded-lg transition-colors font-medium"
>
{t('common:previous', 'Anterior')}
</button>
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => 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 →')}
</button>
</div>
</div>
)}
</div>

View File

@@ -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<SetupStepProps> = ({ onUpdate, onComplete, canContinue }) => {
export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplete, onPrevious, canContinue }) => {
const { t } = useTranslation();
// Get tenant ID
@@ -63,7 +62,7 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ 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<SetupStepProps> = ({ onUpdate, onComplet
</button>
<button
type="button"
onClick={() => 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<SetupStepProps> = ({ onUpdate, onComplet
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span className="text-sm font-medium text-[var(--text-primary)]">
{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' })}
</span>
</div>
{recipes.length >= 1 && (
@@ -606,7 +605,7 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ 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) => (
<option key={option.value} value={option.value}>
@@ -614,6 +613,7 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
</option>
))}
</select>
{errors.yield_unit && <p className="mt-1 text-xs text-[var(--color-error)]">{errors.yield_unit}</p>}
</div>
</div>
@@ -765,23 +765,34 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
</div>
)}
{/* Navigation - Show Next button when minimum requirement met */}
{recipes.length >= 1 && !isAdding && (
<div className="flex items-center justify-between pt-6 border-t border-[var(--border-secondary)] mt-6">
<div className="flex items-center gap-2 text-sm text-[var(--color-success)]">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>
{t('setup_wizard:recipes.minimum_met', '{{count}} recipe(s) added - Ready to continue!', { count: recipes.length })}
</span>
{/* Navigation buttons */}
{!isAdding && (
<div className="flex items-center justify-between gap-4 pt-6 mt-6 border-t border-[var(--border-secondary)]">
<div className="flex gap-2">
<button
type="button"
onClick={onPrevious}
className="px-4 py-2 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] rounded-lg transition-colors font-medium"
>
{t('common:previous', 'Anterior')}
</button>
</div>
<div className="flex items-center gap-3">
{!canContinue && recipes.length === 0 && (
<p className="text-sm text-[var(--color-warning)]">
{t('setup_wizard:recipes.add_minimum', 'Agrega al menos 1 receta para continuar')}
</p>
)}
<button
type="button"
onClick={() => 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 →')}
</button>
</div>
<button
onClick={() => 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')}
</button>
</div>
)}

View File

@@ -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<SetupStepProps> = ({ onUpdate, onComplete, canContinue }) => {
export const ReviewSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplete, onPrevious, canContinue }) => {
const { t } = useTranslation();
// Get tenant ID
@@ -25,7 +27,7 @@ export const ReviewSetupStep: React.FC<SetupStepProps> = ({ 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<SetupStepProps> = ({ onUpdate, onComplete
<p className="text-xs text-[var(--text-tertiary)] truncate">{supplier.email}</p>
)}
</div>
{supplier.is_active && (
{supplier.status === SupplierStatus.ACTIVE && (
<span className="flex-shrink-0 w-2 h-2 bg-green-500 rounded-full" title="Active" />
)}
</div>
@@ -307,16 +309,29 @@ export const ReviewSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplete
</>
)}
{/* Continue button - only shown when used in onboarding context */}
{/* Navigation buttons */}
{onComplete && (
<div className="flex justify-end mt-6 pt-6 border-t border-[var(--border-secondary)]">
<button
onClick={() => 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 →')}
</button>
<div className="flex items-center justify-between gap-4 pt-6 mt-6 border-t border-[var(--border-secondary)]">
<div className="flex gap-2">
<button
type="button"
onClick={onPrevious}
className="px-4 py-2 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] rounded-lg transition-colors font-medium"
>
{t('common:previous', 'Anterior')}
</button>
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => 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 ✓')}
</button>
</div>
</div>
)}
</div>

View File

@@ -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<SetupStepProps> = ({
// 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<SetupStepProps> = ({
const resetForm = () => {
setFormData({
name: '',
supplier_type: 'ingredients',
supplier_type: SupplierType.INGREDIENTS,
contact_person: '',
phone: '',
email: '',
@@ -180,7 +180,7 @@ export const SuppliersSetupStep: React.FC<SetupStepProps> = ({
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
<span className="text-sm font-medium text-[var(--text-primary)]">
{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' })}
</span>
</div>
{suppliers.length >= 1 && (
@@ -441,13 +441,13 @@ export const SuppliersSetupStep: React.FC<SetupStepProps> = ({
{/* Navigation buttons */}
<div className="flex items-center justify-between gap-4 pt-6 mt-6 border-t border-[var(--border-secondary)]">
<div className="flex gap-2">
{!isFirstStep && (
{onPrevious && (
<button
type="button"
onClick={onPrevious}
className="px-4 py-2 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] rounded-lg transition-colors font-medium"
>
{t('common:previous', 'Previous')}
{t('common:previous', 'Anterior')}
</button>
)}
{onSkip && suppliers.length === 0 && (
@@ -462,18 +462,18 @@ export const SuppliersSetupStep: React.FC<SetupStepProps> = ({
</div>
<div className="flex items-center gap-3">
{!canContinue && (
{!canContinue && suppliers.length === 0 && (
<p className="text-sm text-[var(--color-warning)]">
{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')}
</p>
)}
<button
type="button"
onClick={() => 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 →')}
</button>
</div>
</div>

View File

@@ -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<SetupStepProps> = ({ onUpdate, onComplete, canContinue }) => {
export const TeamSetupStep: React.FC<SetupStepProps> = ({ 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<SetupStepProps> = ({ 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<SetupStepProps> = ({ onUpdate, onComplete,
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<span className="text-sm font-medium text-[var(--text-primary)]">
{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' })}
</span>
</div>
</div>
@@ -247,11 +247,10 @@ export const TeamSetupStep: React.FC<SetupStepProps> = ({ 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)]'
} `}
>
<div className="flex items-center gap-2 mb-1">
<span className="text-lg">{option.icon}</span>
@@ -311,16 +310,29 @@ export const TeamSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplete,
</div>
)}
{/* Continue button - only shown when used in onboarding context */}
{onComplete && (
<div className="flex justify-end mt-6 pt-6 border-t border-[var(--border-secondary)]">
<button
onClick={() => 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 →')}
</button>
{/* Navigation buttons */}
{!isAdding && onComplete && (
<div className="flex items-center justify-between gap-4 pt-6 mt-6 border-t border-[var(--border-secondary)]">
<div className="flex gap-2">
<button
type="button"
onClick={onPrevious}
className="px-4 py-2 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] rounded-lg transition-colors font-medium"
>
{t('common:previous', 'Anterior')}
</button>
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => 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 →')}
</button>
</div>
</div>
)}
</div>

View File

@@ -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;
}

View File

@@ -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",

View File

@@ -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"

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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"

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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"

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 (
<div className="flex items-center justify-center min-h-screen">

View File

@@ -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;
}