Imporve onboarding UI
This commit is contained in:
@@ -1844,7 +1844,7 @@ completed_at: TIMESTAMP (nullable)
|
||||
"suppliers": {
|
||||
"title": "Add Suppliers",
|
||||
"description": "Your ingredient and material providers",
|
||||
"min_required": "Add at least {{count}} supplier to continue",
|
||||
"min_required": "Add at least {count} supplier to continue",
|
||||
...
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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)')}
|
||||
|
||||
@@ -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;
|
||||
@@ -492,8 +503,32 @@ const OnboardingWizardContent: React.FC = () => {
|
||||
}
|
||||
|
||||
if (currentStep.id === 'completion') {
|
||||
wizardContext.resetWizard();
|
||||
// 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,67 +618,139 @@ 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">
|
||||
{/* 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)]/80 h-2 sm:h-3 rounded-full transition-all duration-500 ease-out"
|
||||
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">
|
||||
<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>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-lg sm:text-xl font-semibold text-[var(--text-primary)]">
|
||||
{/* Decorative ring */}
|
||||
<div className="absolute inset-0 rounded-2xl bg-[var(--color-primary)]/20 animate-ping" style={{ animationDuration: '3s' }} />
|
||||
</div>
|
||||
<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={() => { }}
|
||||
onPrevious={handleGoToPrevious}
|
||||
|
||||
@@ -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)]">
|
||||
<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'
|
||||
'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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -326,27 +367,33 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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,8 +486,7 @@ 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'
|
||||
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)]'
|
||||
}`}
|
||||
@@ -449,8 +495,7 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
|
||||
</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'
|
||||
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)]'
|
||||
}`}
|
||||
@@ -461,8 +506,7 @@ 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'
|
||||
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)]'
|
||||
}`}
|
||||
@@ -492,8 +536,7 @@ 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
|
||||
<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'
|
||||
}`}>
|
||||
@@ -579,8 +622,7 @@ 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
|
||||
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)]'
|
||||
}`}
|
||||
@@ -600,8 +642,7 @@ 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
|
||||
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)]'
|
||||
}`}
|
||||
@@ -724,8 +765,7 @@ 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
|
||||
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)]'
|
||||
}`}
|
||||
@@ -745,8 +785,7 @@ 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
|
||||
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)]'
|
||||
}`}
|
||||
|
||||
@@ -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,8 +245,26 @@ 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">
|
||||
<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>
|
||||
|
||||
<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"}
|
||||
@@ -213,7 +273,9 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
error={errors.name}
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="transform transition-all duration-200 hover:scale-[1.01]">
|
||||
<Input
|
||||
label="Teléfono"
|
||||
type="tel"
|
||||
@@ -223,9 +285,11 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
error={errors.phone}
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<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,12 +304,14 @@ 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>
|
||||
|
||||
<div className="transform transition-all duration-200 hover:scale-[1.01]">
|
||||
<Input
|
||||
label="Código Postal"
|
||||
placeholder="28001"
|
||||
@@ -255,7 +321,9 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
isRequired
|
||||
maxLength={5}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="transform transition-all duration-200 hover:scale-[1.01]">
|
||||
<Input
|
||||
label="Ciudad (Opcional)"
|
||||
placeholder="Madrid"
|
||||
@@ -264,21 +332,32 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
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>
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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,17 +1007,30 @@ 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)]">
|
||||
{/* 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-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"
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 ? (
|
||||
@@ -274,8 +273,7 @@ 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
|
||||
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)]'
|
||||
} `}
|
||||
@@ -325,8 +323,7 @@ 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)
|
||||
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)]'
|
||||
} `}
|
||||
@@ -429,17 +426,30 @@ 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)]">
|
||||
{/* 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-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"
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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,24 +765,35 @@ 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>
|
||||
</div>
|
||||
{/* 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
|
||||
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"
|
||||
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:next', 'Next')}
|
||||
← {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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Add Ingredient Modal */}
|
||||
|
||||
@@ -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,17 +309,30 @@ 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)]">
|
||||
<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-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"
|
||||
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', 'Completar Configuración ✓')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,8 +247,7 @@ 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
|
||||
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)]'
|
||||
} `}
|
||||
@@ -311,17 +310,30 @@ 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)]">
|
||||
{/* 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-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"
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
11
frontend/src/components/domain/setup-wizard/types.ts
Normal file
11
frontend/src/components/domain/setup-wizard/types.ts
Normal 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;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -732,3 +732,62 @@
|
||||
.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;
|
||||
}
|
||||
@@ -153,8 +153,33 @@ class OnboardingService:
|
||||
# Calculate completion percentage
|
||||
completion_percentage = (len(completed_steps) / len(ONBOARDING_STEPS)) * 100
|
||||
|
||||
# Check if fully completed
|
||||
fully_completed = len(completed_steps) == len(ONBOARDING_STEPS)
|
||||
# Check if fully completed - based on REQUIRED steps only
|
||||
# Define required steps
|
||||
REQUIRED_STEPS = [
|
||||
"user_registered",
|
||||
"setup",
|
||||
"suppliers-setup",
|
||||
"ml-training",
|
||||
"completion"
|
||||
]
|
||||
|
||||
# Get user's subscription tier to determine if bakery-type-selection is required
|
||||
user_registered_data = user_progress_data.get("user_registered", {}).get("data", {})
|
||||
subscription_tier = user_registered_data.get("subscription_tier", "professional")
|
||||
|
||||
# Add bakery-type-selection to required steps for non-enterprise users
|
||||
if subscription_tier != "enterprise":
|
||||
required_steps_for_user = REQUIRED_STEPS + ["bakery-type-selection"]
|
||||
else:
|
||||
required_steps_for_user = REQUIRED_STEPS
|
||||
|
||||
# Check if all required steps are completed
|
||||
required_completed = all(
|
||||
user_progress_data.get(step, {}).get("completed", False)
|
||||
for step in required_steps_for_user
|
||||
)
|
||||
|
||||
fully_completed = required_completed
|
||||
|
||||
return UserProgress(
|
||||
user_id=user_id,
|
||||
@@ -235,16 +260,62 @@ class OnboardingService:
|
||||
async def complete_onboarding(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Mark entire onboarding as complete"""
|
||||
|
||||
# Ensure all steps are completed
|
||||
# Get user's progress
|
||||
progress = await self.get_user_progress(user_id)
|
||||
user_progress_data = await self._get_user_onboarding_data(user_id)
|
||||
|
||||
if not progress.fully_completed:
|
||||
incomplete_steps = [
|
||||
step.step_name for step in progress.steps if not step.completed
|
||||
# Define REQUIRED steps (excluding optional/conditional steps)
|
||||
# These are the minimum steps needed to complete onboarding
|
||||
REQUIRED_STEPS = [
|
||||
"user_registered",
|
||||
"setup", # bakery-type-selection is conditional for enterprise
|
||||
"suppliers-setup",
|
||||
"ml-training",
|
||||
"completion"
|
||||
]
|
||||
|
||||
# Define CONDITIONAL steps that are only required for certain tiers/flows
|
||||
CONDITIONAL_STEPS = {
|
||||
"child-tenants-setup": "enterprise", # Only for enterprise tier
|
||||
"product-categorization": None, # Optional for all
|
||||
"bakery-type-selection": "non-enterprise", # Only for non-enterprise
|
||||
"upload-sales-data": None, # Optional (manual inventory setup is alternative)
|
||||
"inventory-review": None, # Optional (manual inventory setup is alternative)
|
||||
"initial-stock-entry": None, # Optional
|
||||
"recipes-setup": None, # Optional
|
||||
"quality-setup": None, # Optional
|
||||
"team-setup": None, # Optional
|
||||
}
|
||||
|
||||
# Get user's subscription tier
|
||||
user_registered_data = user_progress_data.get("user_registered", {}).get("data", {})
|
||||
subscription_tier = user_registered_data.get("subscription_tier", "professional")
|
||||
|
||||
# Check if all REQUIRED steps are completed
|
||||
incomplete_required_steps = []
|
||||
for step_name in REQUIRED_STEPS:
|
||||
if not user_progress_data.get(step_name, {}).get("completed", False):
|
||||
# Special case: bakery-type-selection is not required for enterprise
|
||||
if step_name == "bakery-type-selection" and subscription_tier == "enterprise":
|
||||
continue
|
||||
incomplete_required_steps.append(step_name)
|
||||
|
||||
# If there are incomplete required steps, reject completion
|
||||
if incomplete_required_steps:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Cannot complete onboarding: incomplete steps: {incomplete_steps}"
|
||||
detail=f"Cannot complete onboarding: incomplete required steps: {incomplete_required_steps}"
|
||||
)
|
||||
|
||||
# Log conditional steps that are incomplete (warning only, not blocking)
|
||||
incomplete_conditional_steps = [
|
||||
step.step_name for step in progress.steps
|
||||
if not step.completed and step.step_name in CONDITIONAL_STEPS
|
||||
]
|
||||
if incomplete_conditional_steps:
|
||||
logger.info(
|
||||
f"User {user_id} completing onboarding with incomplete optional steps: {incomplete_conditional_steps}",
|
||||
extra={"user_id": user_id, "subscription_tier": subscription_tier}
|
||||
)
|
||||
|
||||
# Update user's isOnboardingComplete flag
|
||||
@@ -254,7 +325,11 @@ class OnboardingService:
|
||||
True
|
||||
)
|
||||
|
||||
return {"success": True, "message": "Onboarding completed successfully"}
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Onboarding completed successfully",
|
||||
"optional_steps_skipped": incomplete_conditional_steps
|
||||
}
|
||||
|
||||
def _get_current_step(self, completed_steps: List[str]) -> str:
|
||||
"""Determine current step based on completed steps"""
|
||||
|
||||
Reference in New Issue
Block a user