238 lines
11 KiB
TypeScript
238 lines
11 KiB
TypeScript
// ================================================================
|
||
// frontend/src/components/dashboard/SetupWizardBlocker.tsx
|
||
// ================================================================
|
||
/**
|
||
* Setup Wizard Blocker - Critical Path Onboarding
|
||
*
|
||
* JTBD: "I cannot operate my bakery without basic configuration"
|
||
*
|
||
* This component blocks the entire dashboard when critical setup is incomplete.
|
||
* Shows a full-page wizard to guide users through essential configuration.
|
||
*
|
||
* Triggers when:
|
||
* - 0-2 critical sections complete (<50% progress)
|
||
* - Missing: Ingredients (<3) OR Suppliers (<1) OR Recipes (<1)
|
||
*/
|
||
|
||
import React, { useMemo } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { useTranslation } from 'react-i18next';
|
||
import { AlertCircle, Package, Users, BookOpen, ChevronRight, CheckCircle2, Circle } from 'lucide-react';
|
||
|
||
interface SetupSection {
|
||
id: string;
|
||
title: string;
|
||
description: string;
|
||
icon: React.ElementType;
|
||
path: string;
|
||
isComplete: boolean;
|
||
count: number;
|
||
minimum: number;
|
||
}
|
||
|
||
interface SetupWizardBlockerProps {
|
||
criticalSections: SetupSection[];
|
||
onComplete?: () => void;
|
||
}
|
||
|
||
export function SetupWizardBlocker({ criticalSections = [], onComplete }: SetupWizardBlockerProps) {
|
||
const { t } = useTranslation(['dashboard', 'common']);
|
||
const navigate = useNavigate();
|
||
|
||
// Calculate progress
|
||
const { completedCount, totalCount, progressPercentage, nextSection } = useMemo(() => {
|
||
// Guard against undefined or invalid criticalSections
|
||
if (!criticalSections || !Array.isArray(criticalSections) || criticalSections.length === 0) {
|
||
return {
|
||
completedCount: 0,
|
||
totalCount: 0,
|
||
progressPercentage: 0,
|
||
nextSection: undefined,
|
||
};
|
||
}
|
||
|
||
const completed = criticalSections.filter(s => s.isComplete).length;
|
||
const total = criticalSections.length;
|
||
const percentage = Math.round((completed / total) * 100);
|
||
const next = criticalSections.find(s => !s.isComplete);
|
||
|
||
return {
|
||
completedCount: completed,
|
||
totalCount: total,
|
||
progressPercentage: percentage,
|
||
nextSection: next,
|
||
};
|
||
}, [criticalSections]);
|
||
|
||
const handleSectionClick = (path: string) => {
|
||
navigate(path);
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center p-4" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||
<div className="w-full max-w-3xl">
|
||
{/* Warning Header */}
|
||
<div className="text-center mb-8">
|
||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full mb-4" style={{ backgroundColor: 'var(--color-warning-100)' }}>
|
||
<AlertCircle className="w-10 h-10" style={{ color: 'var(--color-warning-600)' }} />
|
||
</div>
|
||
<h1 className="text-3xl md:text-4xl font-bold mb-3" style={{ color: 'var(--text-primary)' }}>
|
||
⚠️ {t('dashboard:setup_blocker.title', 'Configuración Requerida')}
|
||
</h1>
|
||
<p className="text-lg" style={{ color: 'var(--text-secondary)' }}>
|
||
{t('dashboard:setup_blocker.subtitle', 'Necesitas completar la configuración básica antes de usar el panel de control')}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Progress Card */}
|
||
<div className="bg-[var(--bg-primary)] border-2 border-[var(--border-primary)] rounded-xl shadow-xl p-8">
|
||
{/* Progress Bar */}
|
||
<div className="mb-8">
|
||
<div className="flex items-center justify-between text-sm mb-3">
|
||
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||
{t('dashboard:setup_blocker.progress', 'Progreso de Configuración')}
|
||
</span>
|
||
<span className="font-bold" style={{ color: 'var(--color-primary)' }}>
|
||
{completedCount}/{totalCount} ({progressPercentage}%)
|
||
</span>
|
||
</div>
|
||
<div className="w-full h-3 bg-[var(--bg-tertiary)] rounded-full overflow-hidden">
|
||
<div
|
||
className="h-full transition-all duration-500 ease-out"
|
||
style={{
|
||
width: `${progressPercentage}%`,
|
||
background: 'linear-gradient(90deg, var(--color-primary), var(--color-success))',
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Critical Sections List */}
|
||
<div className="space-y-4 mb-8">
|
||
<h3 className="text-lg font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
|
||
{t('dashboard:setup_blocker.required_steps', 'Pasos Requeridos')}
|
||
</h3>
|
||
|
||
{criticalSections.map((section, index) => {
|
||
const Icon = section.icon || (() => <div>⚙️</div>);
|
||
const isNext = section === nextSection;
|
||
|
||
return (
|
||
<button
|
||
key={section.id}
|
||
onClick={() => handleSectionClick(section.path)}
|
||
className={`w-full p-5 rounded-lg border-2 transition-all duration-200 text-left group ${
|
||
section.isComplete
|
||
? 'border-[var(--color-success)]/30 bg-[var(--color-success)]/5'
|
||
: isNext
|
||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5 hover:bg-[var(--color-primary)]/10 shadow-md'
|
||
: 'border-[var(--border-secondary)] bg-[var(--bg-secondary)] hover:border-[var(--color-primary)] hover:bg-[var(--bg-primary)]'
|
||
}`}
|
||
>
|
||
<div className="flex items-start gap-4">
|
||
{/* Step Number / Status */}
|
||
<div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm ${
|
||
section.isComplete
|
||
? 'bg-[var(--color-success)] text-white'
|
||
: isNext
|
||
? 'bg-[var(--color-primary)] text-white'
|
||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)]'
|
||
}`}>
|
||
{section.isComplete ? (
|
||
<CheckCircle2 className="w-5 h-5" />
|
||
) : (
|
||
<span>{index + 1}</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Section Icon */}
|
||
<div className={`w-12 h-12 rounded-full flex items-center justify-center ${
|
||
section.isComplete
|
||
? 'bg-[var(--color-success)]/20 text-[var(--color-success)]'
|
||
: isNext
|
||
? 'bg-[var(--color-primary)]/20 text-[var(--color-primary)]'
|
||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)]'
|
||
}`}>
|
||
<Icon className="w-6 h-6" />
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<h4 className="font-bold text-lg" style={{ color: 'var(--text-primary)' }}>
|
||
{section.title}
|
||
</h4>
|
||
{isNext && !section.isComplete && (
|
||
<span className="px-2 py-0.5 bg-[var(--color-primary)] text-white rounded-full text-xs font-semibold">
|
||
{t('dashboard:setup_blocker.next', 'Siguiente')}
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
<p className="text-sm mb-2" style={{ color: 'var(--text-secondary)' }}>
|
||
{section.description}
|
||
</p>
|
||
|
||
<div className="flex items-center gap-2 text-sm">
|
||
{section.isComplete ? (
|
||
<span className="font-semibold" style={{ color: 'var(--color-success)' }}>
|
||
✅ {section.count} {t('dashboard:setup_blocker.added', 'agregado(s)')}
|
||
</span>
|
||
) : (
|
||
<span className="font-semibold" style={{ color: 'var(--color-warning-700)' }}>
|
||
⚠️ {t('dashboard:setup_blocker.minimum_required', 'Mínimo requerido')}: {section.minimum}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Chevron */}
|
||
<ChevronRight className={`w-6 h-6 flex-shrink-0 transition-transform group-hover:translate-x-1 ${
|
||
isNext ? 'text-[var(--color-primary)]' : 'text-[var(--text-tertiary)]'
|
||
}`} />
|
||
</div>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* Next Step CTA */}
|
||
{nextSection && (
|
||
<div className="p-5 rounded-xl border-2 border-[var(--color-primary)]/30" style={{ backgroundColor: 'var(--color-primary)]/5' }}>
|
||
<div className="flex items-start gap-4">
|
||
<AlertCircle className="w-6 h-6 flex-shrink-0" style={{ color: 'var(--color-primary)' }} />
|
||
<div className="flex-1">
|
||
<h4 className="font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
|
||
👉 {t('dashboard:setup_blocker.start_with', 'Empieza por')}: {nextSection.title}
|
||
</h4>
|
||
<p className="text-sm mb-4" style={{ color: 'var(--text-secondary)' }}>
|
||
{nextSection.description}
|
||
</p>
|
||
<button
|
||
onClick={() => handleSectionClick(nextSection.path)}
|
||
className="px-6 py-3 rounded-lg font-semibold transition-all duration-200 hover:scale-105 active:scale-95 shadow-lg hover:shadow-xl inline-flex items-center gap-2"
|
||
style={{
|
||
background: 'linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%)',
|
||
color: 'white',
|
||
}}
|
||
>
|
||
{t('dashboard:setup_blocker.configure_now', 'Configurar Ahora')} {nextSection.title}
|
||
<ChevronRight className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Help Text */}
|
||
<div className="mt-6 pt-6 border-t" style={{ borderColor: 'var(--border-secondary)' }}>
|
||
<p className="text-sm text-center" style={{ color: 'var(--text-tertiary)' }}>
|
||
💡 {t('dashboard:setup_blocker.help_text', 'Una vez completes estos pasos, podrás acceder al panel de control completo y comenzar a usar todas las funciones de IA')}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|