feat: Enhance onboarding wizard UX with improved feedback and completion

This commit adds significant UX improvements to multiple onboarding steps:

**1. Recipes Setup Step:**
- Fixed double next button issue (removed duplicate navigation button)
- Filtered finished products dropdown to show only 'finished_product' type ingredients
- Users can now only select appropriate finished products for recipes

**2. File Upload Step:**
- Added comprehensive validation success state with detailed feedback
- Shows file name, rows found, and unique products count after validation
- Enhanced error display with helpful troubleshooting tips
- Clear visual distinction between file selected, validation success, and processing states
- Improved user confidence by clearly communicating validation results

**3. Completion Step:**
- Complete redesign with animated success icon and gradient text
- Added 4 quick access cards for Analytics, Inventory, Procurement, and Production
- Interactive hover effects on quick access cards (scale and shadow)
- New "Tips for Success" section with actionable advice
- Enhanced primary CTA button with better sizing and prominence
- More engaging and valuable final step that guides users to next actions

All changes use global CSS variables for proper dark mode support and maintain
consistent design language throughout the application.
This commit is contained in:
Claude
2025-11-12 15:03:33 +00:00
parent 2c9d43e887
commit ca090125f7
3 changed files with 172 additions and 64 deletions

View File

@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Button } from '../../../ui/Button';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { ChartBar, ShoppingCart, Users, TrendingUp, Zap, CheckCircle2 } from 'lucide-react';
interface CompletionStepProps {
onNext: () => void;
@@ -30,20 +31,21 @@ export const CompletionStep: React.FC<CompletionStepProps> = ({
};
return (
<div className="text-center space-y-8">
{/* Success Icon */}
<div className="mx-auto w-24 h-24 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<svg className="w-12 h-12 text-[var(--color-success)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<div className="text-center space-y-8 max-w-5xl mx-auto">
{/* 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>
</div>
{/* Success Message */}
<div className="space-y-4">
<h1 className="text-3xl font-bold text-[var(--text-primary)]">
<h1 className="text-4xl font-bold bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-success)] bg-clip-text text-transparent">
{t('onboarding:completion.congratulations', '¡Felicidades! Tu Sistema Está Listo')}
</h1>
<p className="text-lg text-[var(--text-secondary)] max-w-2xl mx-auto">
<p className="text-xl text-[var(--text-secondary)] max-w-2xl mx-auto">
{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>
@@ -140,49 +142,109 @@ export const CompletionStep: React.FC<CompletionStepProps> = ({
</div>
</div>
{/* Next Steps */}
<div className="bg-gradient-to-r from-[var(--color-primary)]/10 to-[var(--color-primary)]/5 border border-[var(--color-primary)]/20 rounded-lg p-6 max-w-2xl mx-auto text-left">
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-[var(--color-primary)] text-white rounded-full flex items-center justify-center text-xl font-bold flex-shrink-0">
🚀
</div>
<div>
<h3 className="font-semibold text-lg mb-2 text-[var(--text-primary)]">{t('onboarding:completion.ready_to_start', '¡Listo para Empezar!')}</h3>
<p className="text-sm text-[var(--text-secondary)] mb-3">
{t('onboarding:completion.explore_message', 'Ahora puedes explorar el panel de control y comenzar a gestionar tu panadería con inteligencia artificial.')}
{/* Quick Access Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 max-w-4xl 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"
>
<ChartBar 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">
{t('onboarding:completion.quick.analytics', 'Analíticas')}
</h4>
<p className="text-xs text-[var(--text-secondary)]">
{t('onboarding:completion.quick.analytics_desc', 'Ver predicciones y métricas')}
</p>
<div className="space-y-2">
<div className="flex items-start gap-2 text-sm">
<svg className="w-4 h-4 text-[var(--color-primary)] mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<span className="text-[var(--text-secondary)]">{t('onboarding:completion.view_analytics', 'Ve análisis y predicciones de demanda')}</span>
</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"
>
<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">
{t('onboarding:completion.quick.inventory', 'Inventario')}
</h4>
<p className="text-xs 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"
>
<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">
{t('onboarding:completion.quick.procurement', 'Compras')}
</h4>
<p className="text-xs 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"
>
<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">
{t('onboarding:completion.quick.production', 'Producción')}
</h4>
<p className="text-xs text-[var(--text-secondary)]">
{t('onboarding:completion.quick.production_desc', 'Planificar producción')}
</p>
</button>
</div>
<div className="flex items-start gap-2 text-sm">
<svg className="w-4 h-4 text-[var(--color-primary)] mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-[var(--text-secondary)]">{t('onboarding:completion.manage_operations', 'Gestiona producción y operaciones diarias')}</span>
{/* 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>
<div className="flex items-start gap-2 text-sm">
<svg className="w-4 h-4 text-[var(--color-primary)] mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
<span className="text-[var(--text-secondary)]">{t('onboarding:completion.optimize_costs', 'Optimiza costos y reduce desperdicios')}</span>
<div className="flex-1">
<h3 className="font-bold text-lg mb-3 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">
<div className="flex items-start gap-2">
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" />
<span className="text-[var(--text-secondary)]">
{t('onboarding:completion.tip1', 'Revisa el dashboard diariamente para insights')}
</span>
</div>
<div className="flex items-start gap-2">
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" />
<span className="text-[var(--text-secondary)]">
{t('onboarding:completion.tip2', 'Actualiza el inventario regularmente')}
</span>
</div>
<div className="flex items-start gap-2">
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" />
<span className="text-[var(--text-secondary)]">
{t('onboarding:completion.tip3', 'Usa las predicciones de IA para planificar')}
</span>
</div>
<div className="flex items-start gap-2">
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" />
<span className="text-[var(--text-secondary)]">
{t('onboarding:completion.tip4', 'Invita a tu equipo para colaborar')}
</span>
</div>
</div>
</div>
</div>
</div>
{/* Action Buttons */}
{/* Primary Action Button */}
<div className="flex justify-center items-center pt-4">
<Button
onClick={handleExploreDashboard}
size="lg"
className="px-8"
className="px-12 py-4 text-lg font-semibold shadow-lg hover:shadow-xl transition-all"
>
{t('onboarding:completion.go_to_dashboard', 'Ir al Panel de Control →')}
{t('onboarding:completion.go_to_dashboard', 'Comenzar a Usar el Sistema →')}
</Button>
</div>

View File

@@ -39,6 +39,8 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
const [error, setError] = useState<string>('');
const [progressState, setProgressState] = useState<ProgressState | null>(null);
const [showGuide, setShowGuide] = useState(false);
const [validationSuccess, setValidationSuccess] = useState(false);
const [validationDetails, setValidationDetails] = useState<{rows: number, products: number} | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const currentTenant = useCurrentTenant();
@@ -101,6 +103,13 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
throw new Error(errorMsg);
}
// Show validation success feedback
setValidationSuccess(true);
setValidationDetails({
rows: validationResult.total_rows || 0,
products: validationResult.product_list?.length || 0
});
// Step 2: Extract product list
setProgressState({
stage: 'analyzing',
@@ -158,6 +167,8 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
setSelectedFile(null);
setError('');
setProgressState(null);
setValidationSuccess(false);
setValidationDetails(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
@@ -215,14 +226,14 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
)}
{/* Selected File Preview */}
{selectedFile && !isProcessing && (
<div className="border border-[var(--color-success)] bg-[var(--color-success)]/5 rounded-lg p-3 md:p-4">
{selectedFile && !isProcessing && !validationSuccess && (
<div className="border-2 border-[var(--color-primary)] bg-[var(--color-primary)]/5 rounded-lg p-3 md:p-4">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 md:gap-3 min-w-0">
<FileText className="w-8 h-8 md:w-10 md:h-10 text-[var(--color-success)] flex-shrink-0" />
<FileText className="w-8 h-8 md:w-10 md:h-10 text-[var(--color-primary)] flex-shrink-0" />
<div className="min-w-0">
<p className="font-medium text-[var(--text-primary)] text-sm md:text-base truncate">{selectedFile.name}</p>
<p className="text-sm text-[var(--text-secondary)]">
<p className="text-xs text-[var(--text-secondary)]">
{(selectedFile.size / 1024).toFixed(2)} KB
</p>
</div>
@@ -237,6 +248,40 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
</div>
)}
{/* Validation Success State */}
{selectedFile && validationSuccess && !isProcessing && validationDetails && (
<div className="border-2 border-[var(--color-success)] bg-[var(--color-success)]/10 rounded-lg p-4">
<div className="flex items-start gap-3">
<CheckCircle2 className="w-6 h-6 text-[var(--color-success)] flex-shrink-0 mt-0.5" />
<div className="flex-1">
<p className="font-semibold text-[var(--color-success)] mb-2">
{t('onboarding:file_upload.validation_success', '¡Archivo validado correctamente!')}
</p>
<div className="space-y-1 text-sm">
<div className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">{t('onboarding:file_upload.file_name', 'Archivo:')}</span>
<span className="font-medium text-[var(--text-primary)]">{selectedFile.name}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">{t('onboarding:file_upload.rows_found', 'Registros encontrados:')}</span>
<span className="font-medium text-[var(--text-primary)]">{validationDetails.rows}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">{t('onboarding:file_upload.products_found', 'Productos únicos:')}</span>
<span className="font-medium text-[var(--text-primary)]">{validationDetails.products}</span>
</div>
</div>
<button
onClick={handleRemoveFile}
className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)] mt-2 hover:underline"
>
{t('onboarding:file_upload.change_file', 'Cambiar archivo')}
</button>
</div>
</div>
</div>
)}
{/* Progress Indicator */}
{isProcessing && progressState && (
<div className="border border-[var(--color-primary)] rounded-lg p-6 bg-[var(--color-primary)]/5">
@@ -260,12 +305,24 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
{/* Error Display */}
{error && (
<div className="bg-[var(--color-danger)]/10 border border-[var(--color-danger)]/20 rounded-lg p-4">
<div className="flex items-start gap-2">
<AlertCircle className="w-5 h-5 text-[var(--color-danger)] flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium text-[var(--color-danger)] mb-1">Error</p>
<p className="text-sm text-[var(--text-secondary)]">{error}</p>
<div className="bg-[var(--color-error)]/10 border-2 border-[var(--color-error)] rounded-lg p-4">
<div className="flex items-start gap-3">
<AlertCircle className="w-6 h-6 text-[var(--color-error)] flex-shrink-0 mt-0.5" />
<div className="flex-1">
<p className="font-semibold text-[var(--color-error)] mb-2">
{t('onboarding:file_upload.validation_failed', 'Error al validar el archivo')}
</p>
<p className="text-sm text-[var(--text-secondary)] mb-3">{error}</p>
<div className="bg-[var(--bg-secondary)] rounded p-3 text-xs space-y-1">
<p className="font-medium text-[var(--text-primary)] mb-1">
{t('onboarding:file_upload.error_tips', 'Consejos para solucionar el problema:')}
</p>
<ul className="list-disc list-inside space-y-0.5 text-[var(--text-secondary)]">
<li>{t('onboarding:file_upload.tip_1', 'Verifica que el archivo tenga las columnas: Fecha, Producto, Cantidad')}</li>
<li>{t('onboarding:file_upload.tip_2', 'Asegúrate de que las fechas estén en formato YYYY-MM-DD')}</li>
<li>{t('onboarding:file_upload.tip_3', 'Comprueba que no haya filas vacías o datos incorrectos')}</li>
</ul>
</div>
</div>
</div>
</div>

View File

@@ -565,7 +565,9 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
className={`w-full px-3 py-2 bg-[var(--bg-primary)] border ${errors.finished_product_id ? '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)]`}
>
<option value="">{t('setup_wizard:recipes.placeholders.finished_product', 'Select finished product...')}</option>
{ingredients.map((ing) => (
{ingredients
.filter((ing) => ing.product_type === 'finished_product')
.map((ing) => (
<option key={ing.id} value={ing.id}>
{ing.name} ({ing.unit_of_measure})
</option>
@@ -793,19 +795,6 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
tenantId={tenantId}
context="recipe"
/>
{/* Continue button - only shown when used in onboarding context */}
{onComplete && (
<div className="flex justify-end mt-6 pt-6 border-[var(--border-secondary)]">
<button
onClick={() => onComplete()}
disabled={canContinue === false}
className="px-6 py-3 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
>
{t('setup_wizard:navigation.continue', 'Continue →')}
</button>
</div>
)}
</div>
);
};