Fix onboarding UI

This commit is contained in:
Urtzi Alfaro
2026-01-05 19:42:35 +01:00
parent 6b14f330e6
commit 18627f02d4
16 changed files with 680 additions and 382 deletions

View File

@@ -39,249 +39,161 @@ export const CompletionStep: React.FC<CompletionStepProps> = ({
}; };
return ( return (
<div className="text-center space-y-10 max-w-5xl mx-auto px-4 animate-fade-in"> <div className="text-center space-y-8 max-w-4xl mx-auto px-4 animate-fade-in">
{/* Confetti effect */} {/* Success Icon */}
<div className="absolute inset-0 pointer-events-none overflow-hidden"> <div className="relative mx-auto w-32 h-32">
<div className="absolute top-0 left-1/4 text-4xl animate-bounce-subtle" style={{ animationDelay: '0s' }}>🎉</div> <div className="absolute inset-0 bg-[var(--color-success)]/20 rounded-full animate-pulse"></div>
<div className="absolute top-10 right-1/4 text-3xl animate-bounce-subtle" style={{ animationDelay: '0.5s' }}></div> <div className="relative w-32 h-32 bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success)]/80 rounded-full flex items-center justify-center shadow-xl">
<div className="absolute top-5 left-1/2 text-4xl animate-bounce-subtle" style={{ animationDelay: '1s' }}>🎊</div> <CheckCircle2 className="w-16 h-16 text-white" />
</div>
{/* Animated Success Icon */}
<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>
</div> </div>
{/* Success Message */} {/* Success Message */}
<div className="space-y-5 animate-slide-up"> <div className="space-y-4">
<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" style={{ backgroundSize: '200% auto' }}> <h1 className="text-3xl md:text-4xl font-bold text-[var(--text-primary)]">
{t('onboarding:completion.congratulations', '¡Felicidades! Tu Sistema Está Listo')} {t('onboarding:completion.congratulations', 'Congratulations! Your System Is Ready')}
</h1> </h1>
<p className="text-lg md:text-xl text-[var(--text-secondary)] max-w-3xl mx-auto leading-relaxed"> <p className="text-base md:text-lg 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 })} {t('onboarding:completion.all_configured', 'You have successfully configured {name} with our intelligent management system. Everything is ready to start optimizing your bakery.', { name: currentTenant?.name })}
</p> </p>
</div> </div>
{/* What You Configured */} {/* What You Configured */}
<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"> <div className="bg-[var(--bg-secondary)] rounded-xl p-6 md:p-7 max-w-2xl mx-auto text-left border border-[var(--border-color)]">
<h3 className="font-bold text-xl md:text-2xl mb-6 text-center text-[var(--text-primary)] flex items-center justify-center gap-3"> <h3 className="font-bold text-lg md:text-xl mb-5 text-center text-[var(--text-primary)]">
<span className="text-2xl">📋</span> {t('onboarding:completion.what_configured', 'What You Have Configured')}
{t('onboarding:completion.what_configured', 'Lo Que Has Configurado')}
</h3> </h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="flex items-start gap-3"> <div className="flex items-center gap-2">
<div className="w-6 h-6 bg-[var(--color-success)]/10 rounded flex items-center justify-center flex-shrink-0 mt-0.5"> <CheckCircle2 className="w-5 h-5 text-[var(--color-success)] flex-shrink-0" />
<svg className="w-4 h-4 text-[var(--color-success)]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <span className="text-sm text-[var(--text-primary)]">{t('onboarding:completion.bakery_info', 'Bakery Information')}</span>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div> </div>
<div> <div className="flex items-center gap-2">
<p className="font-medium text-sm text-[var(--text-primary)]">{t('onboarding:completion.bakery_info', 'Información de Panadería')}</p> <CheckCircle2 className="w-5 h-5 text-[var(--color-success)] flex-shrink-0" />
<p className="text-xs text-[var(--text-secondary)]">{t('onboarding:completion.bakery_info_desc', 'Datos básicos registrados')}</p> <span className="text-sm text-[var(--text-primary)]">{t('onboarding:completion.inventory_ai', 'AI Inventory')}</span>
</div> </div>
<div className="flex items-center gap-2">
<CheckCircle2 className="w-5 h-5 text-[var(--color-success)] flex-shrink-0" />
<span className="text-sm text-[var(--text-primary)]">{t('onboarding:completion.suppliers_added', 'Suppliers Added')}</span>
</div> </div>
<div className="flex items-center gap-2">
<div className="flex items-start gap-3"> <CheckCircle2 className="w-5 h-5 text-[var(--color-success)] flex-shrink-0" />
<div className="w-6 h-6 bg-[var(--color-success)]/10 rounded flex items-center justify-center flex-shrink-0 mt-0.5"> <span className="text-sm text-[var(--text-primary)]">{t('onboarding:completion.recipes_configured', 'Recipes Configured')}</span>
<svg className="w-4 h-4 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> </div>
<div> <div className="flex items-center gap-2">
<p className="font-medium text-sm text-[var(--text-primary)]">{t('onboarding:completion.inventory_ai', 'Inventario con IA')}</p> <CheckCircle2 className="w-5 h-5 text-[var(--color-success)] flex-shrink-0" />
<p className="text-xs text-[var(--text-secondary)]">{t('onboarding:completion.inventory_ai_desc', 'Productos analizados y categorizados')}</p> <span className="text-sm text-[var(--text-primary)]">{t('onboarding:completion.quality_set', 'Quality Standards')}</span>
</div>
</div>
<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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<div>
<p className="font-medium text-sm text-[var(--text-primary)]">{t('onboarding:completion.suppliers_added', 'Proveedores Agregados')}</p>
<p className="text-xs text-[var(--text-secondary)]">{t('onboarding:completion.suppliers_added_desc', 'Red de suministro configurada')}</p>
</div>
</div>
<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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<div>
<p className="font-medium text-sm text-[var(--text-primary)]">{t('onboarding:completion.recipes_configured', 'Recetas Configuradas')}</p>
<p className="text-xs text-[var(--text-secondary)]">{t('onboarding:completion.recipes_configured_desc', 'Producción lista para usar')}</p>
</div>
</div>
<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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<div>
<p className="font-medium text-sm text-[var(--text-primary)]">{t('onboarding:completion.quality_set', 'Calidad Establecida')}</p>
<p className="text-xs text-[var(--text-secondary)]">{t('onboarding:completion.quality_set_desc', 'Estándares definidos')}</p>
</div>
</div>
<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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<div>
<p className="font-medium text-sm text-[var(--text-primary)]">{t('onboarding:completion.team_invited', 'Equipo Invitado')}</p>
<p className="text-xs text-[var(--text-secondary)]">{t('onboarding:completion.team_invited_desc', 'Colaboradores configurados')}</p>
</div>
</div>
<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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<div>
<p className="font-medium text-sm text-[var(--text-primary)]">{t('onboarding:completion.ml_model_trained', 'Modelo IA Entrenado')}</p>
<p className="text-xs text-[var(--text-secondary)]">{t('onboarding:completion.ml_model_trained_desc', 'Predicciones personalizadas activas')}</p>
</div> </div>
<div className="flex items-center gap-2">
<CheckCircle2 className="w-5 h-5 text-[var(--color-success)] flex-shrink-0" />
<span className="text-sm text-[var(--text-primary)]">{t('onboarding:completion.team_invited', 'Team Members')}</span>
</div> </div>
</div> </div>
</div> </div>
{/* Quick Access Cards */} {/* Quick Access Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5 max-w-5xl mx-auto"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-3xl mx-auto">
<button <button
onClick={() => navigate('/app/dashboard')} onClick={() => navigate('/app/dashboard')}
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" className="p-4 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg transition-all duration-200 hover:shadow-lg text-left group"
> >
<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" /> <BarChart className="w-8 h-8 text-[var(--color-primary)] mb-2 group-hover:scale-110 transition-transform" />
<h4 className="font-bold text-base md:text-lg text-[var(--text-primary)] mb-1.5"> <h4 className="font-semibold text-sm text-[var(--text-primary)]">
{t('onboarding:completion.quick.analytics', 'Analíticas')} {t('onboarding:completion.quick.analytics', 'Analytics')}
</h4> </h4>
<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>
<button <button
onClick={() => navigate('/app/inventory')} onClick={() => navigate('/app/inventory')}
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" className="p-4 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg transition-all duration-200 hover:shadow-lg text-left group"
> >
<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" /> <ShoppingCart className="w-8 h-8 text-[var(--color-success)] mb-2 group-hover:scale-110 transition-transform" />
<h4 className="font-bold text-base md:text-lg text-[var(--text-primary)] mb-1.5"> <h4 className="font-semibold text-sm text-[var(--text-primary)]">
{t('onboarding:completion.quick.inventory', 'Inventario')} {t('onboarding:completion.quick.inventory', 'Inventory')}
</h4> </h4>
<p className="text-xs md:text-sm text-[var(--text-secondary)]">
{t('onboarding:completion.quick.inventory_desc', 'Gestionar stock y productos')}
</p>
</button> </button>
<button <button
onClick={() => navigate('/app/procurement')} onClick={() => navigate('/app/procurement')}
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" className="p-4 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg transition-all duration-200 hover:shadow-lg text-left group"
> >
<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" /> <Users className="w-8 h-8 text-[var(--color-info)] mb-2 group-hover:scale-110 transition-transform" />
<h4 className="font-bold text-base md:text-lg text-[var(--text-primary)] mb-1.5"> <h4 className="font-semibold text-sm text-[var(--text-primary)]">
{t('onboarding:completion.quick.procurement', 'Compras')} {t('onboarding:completion.quick.procurement', 'Purchases')}
</h4> </h4>
<p className="text-xs md:text-sm text-[var(--text-secondary)]">
{t('onboarding:completion.quick.procurement_desc', 'Gestionar pedidos')}
</p>
</button> </button>
<button <button
onClick={() => navigate('/app/production')} onClick={() => navigate('/app/production')}
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" className="p-4 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg transition-all duration-200 hover:shadow-lg text-left group"
> >
<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" /> <TrendingUp className="w-8 h-8 text-[var(--color-warning)] mb-2 group-hover:scale-110 transition-transform" />
<h4 className="font-bold text-base md:text-lg text-[var(--text-primary)] mb-1.5"> <h4 className="font-semibold text-sm text-[var(--text-primary)]">
{t('onboarding:completion.quick.production', 'Producción')} {t('onboarding:completion.quick.production', 'Production')}
</h4> </h4>
<p className="text-xs md:text-sm text-[var(--text-secondary)]">
{t('onboarding:completion.quick.production_desc', 'Planificar producción')}
</p>
</button> </button>
</div> </div>
{/* Tips for Success */} {/* Tips for Success */}
<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="bg-gradient-to-r from-[var(--color-primary)]/5 to-[var(--color-success)]/5 border border-[var(--color-primary)]/20 rounded-xl p-6 max-w-2xl mx-auto text-left">
<div className="flex items-start gap-5"> <div className="flex items-start gap-4">
<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"> <div className="w-12 h-12 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-success)] text-white rounded-lg flex items-center justify-center flex-shrink-0">
<Zap className="w-7 h-7 md:w-8 md:h-8" /> <Zap className="w-6 h-6" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h3 className="font-bold text-xl md:text-2xl mb-4 text-[var(--text-primary)]"> <h3 className="font-bold text-lg mb-3 text-[var(--text-primary)]">
{t('onboarding:completion.tips_title', 'Consejos para Maximizar tu Éxito')} {t('onboarding:completion.tips_title', 'Tips to Maximize Your Success')}
</h3> </h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm"> <ul className="space-y-2 text-sm text-[var(--text-secondary)]">
<div className="flex items-start gap-2"> <li className="flex items-start gap-2">
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" /> <CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" />
<span className="text-[var(--text-secondary)]"> <span>{t('onboarding:completion.tip1', 'Review the dashboard daily for insights')}</span>
{t('onboarding:completion.tip1', 'Revisa el dashboard diariamente para insights')} </li>
</span> <li className="flex items-start gap-2">
</div>
<div className="flex items-start gap-2">
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" /> <CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" />
<span className="text-[var(--text-secondary)]"> <span>{t('onboarding:completion.tip2', 'Update inventory regularly')}</span>
{t('onboarding:completion.tip2', 'Actualiza el inventario regularmente')} </li>
</span> <li className="flex items-start gap-2">
</div>
<div className="flex items-start gap-2">
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" /> <CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" />
<span className="text-[var(--text-secondary)]"> <span>{t('onboarding:completion.tip3', 'Use AI predictions for planning')}</span>
{t('onboarding:completion.tip3', 'Usa las predicciones de IA para planificar')} </li>
</span> <li className="flex items-start gap-2">
</div>
<div className="flex items-start gap-2">
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" /> <CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" />
<span className="text-[var(--text-secondary)]"> <span>{t('onboarding:completion.tip4', 'Invite your team to collaborate')}</span>
{t('onboarding:completion.tip4', 'Invita a tu equipo para colaborar')} </li>
</span> </ul>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
{/* Primary Action Button */} {/* Primary Action Button */}
<div className="flex justify-center items-center pt-6"> <div className="flex justify-center pt-4">
<Button <Button
onClick={handleExploreDashboard} onClick={handleExploreDashboard}
size="lg" size="lg"
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)]" className="px-12 py-4 text-base md:text-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-200"
> >
{t('onboarding:completion.go_to_dashboard', 'Comenzar a Usar el Sistema')} 🚀 {t('onboarding:completion.go_to_dashboard', 'Start Using the System')}
</Button> </Button>
</div> </div>
{/* Help Text */} {/* Help Text */}
<div className="text-sm text-[var(--text-tertiary)]"> <div className="text-sm text-[var(--text-tertiary)]">
{t('onboarding:completion.need_help', '¿Necesitas ayuda? Visita nuestra')}{' '} {t('onboarding:completion.need_help', 'Need help? Visit our')}{' '}
<a <a
href="/help" href="/help"
className="text-[var(--color-primary)] hover:underline" className="text-[var(--color-primary)] hover:underline"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{t('onboarding:completion.user_guide', 'guía de usuario')} {t('onboarding:completion.user_guide', 'user guide')}
</a>{' '} </a>{' '}
{t('onboarding:completion.or_contact', 'o contacta a nuestro')}{' '} {t('onboarding:completion.or_contact', 'or contact our')}{' '}
<a <a
href="mailto:support@bakery-ia.com" href="mailto:support@bakery-ia.com"
className="text-[var(--color-primary)] hover:underline" className="text-[var(--color-primary)] hover:underline"
> >
{t('onboarding:completion.support_team', 'equipo de soporte')} {t('onboarding:completion.support_team', 'support team')}
</a> </a>
</div> </div>
</div> </div>

View File

@@ -25,7 +25,9 @@ export interface RecipeTemplate {
totalTime?: number; totalTime?: number;
ingredients: RecipeIngredientTemplate[]; ingredients: RecipeIngredientTemplate[];
instructions?: string; instructions?: string;
instructionsKey?: string; // Translation key for instructions
tips?: string[]; tips?: string[];
tipsKeys?: string[]; // Translation keys for tips
} }
/** /**
@@ -159,10 +161,15 @@ export const CAKE_RECIPES: RecipeTemplate[] = [
{ ingredientName: 'Polvo de Hornear', quantity: 10, unit: MeasurementUnit.GRAMS, alternatives: ['Baking Powder'] }, { ingredientName: 'Polvo de Hornear', quantity: 10, unit: MeasurementUnit.GRAMS, alternatives: ['Baking Powder'] },
], ],
instructions: '1. Beat eggs with sugar until fluffy\n2. Add vanilla\n3. Gently fold in flour and baking powder\n4. Pour into greased pan\n5. Bake at 180°C for 35 minutes', instructions: '1. Beat eggs with sugar until fluffy\n2. Add vanilla\n3. Gently fold in flour and baking powder\n4. Pour into greased pan\n5. Bake at 180°C for 35 minutes',
instructionsKey: 'recipe_templates:bizcochuelo.instructions',
tips: [ tips: [
'Do not overmix after adding flour', 'Do not overmix after adding flour',
'Test doneness with toothpick', 'Test doneness with toothpick',
], ],
tipsKeys: [
'recipe_templates:bizcochuelo.tip1',
'recipe_templates:bizcochuelo.tip2',
],
}, },
]; ];

View File

@@ -51,193 +51,72 @@ export const CompletionStep: React.FC<SetupStepProps> = ({ onComplete, onUpdate
}, },
]; ];
const tips = [
{
icon: '💡',
title: t('setup_wizard:completion.tip1_title', 'Keep Inventory Updated'),
description: t('setup_wizard:completion.tip1_desc', 'Regularly update stock levels to get accurate cost calculations and low-stock alerts'),
},
{
icon: '📊',
title: t('setup_wizard:completion.tip2_title', 'Monitor Quality Metrics'),
description: t('setup_wizard:completion.tip2_desc', 'Use quality checks during production to identify issues early and maintain consistency'),
},
{
icon: '🎯',
title: t('setup_wizard:completion.tip3_title', 'Review Analytics Weekly'),
description: t('setup_wizard:completion.tip3_desc', 'Check your production analytics every week to optimize recipes and reduce waste'),
},
{
icon: '🤝',
title: t('setup_wizard:completion.tip4_title', 'Maintain Supplier Relationships'),
description: t('setup_wizard:completion.tip4_desc', 'Keep supplier information current and track order performance for better partnerships'),
},
];
const handleGoToDashboard = () => { const handleGoToDashboard = () => {
onComplete?.({ completed: true }); onComplete?.({ completed: true });
navigate('/app/dashboard'); navigate('/app/dashboard');
}; };
return ( return (
<div className="space-y-8 max-w-4xl mx-auto"> <div className="text-center space-y-8 py-8 max-w-3xl mx-auto">
{/* Celebration Header */} {/* Success Icon */}
<div className="text-center"> <div className="mx-auto w-24 h-24 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<div className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-[var(--color-success)] to-[var(--color-primary)] rounded-full mb-6 animate-bounce"> <svg className="w-12 h-12 text-[var(--color-success)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg> </svg>
</div> </div>
<h1 className="text-3xl md:text-4xl font-bold text-[var(--text-primary)] mb-3">
{t('setup_wizard:completion.title', '🎉 Setup Complete!')} {/* Completion Message */}
<div className="space-y-4">
<h1 className="text-3xl font-bold text-[var(--text-primary)]">
{t('setup_wizard:completion.title', 'Setup Complete!')}
</h1> </h1>
<p className="text-lg text-[var(--text-secondary)] max-w-2xl mx-auto"> <p className="text-lg text-[var(--text-secondary)]">
{t('setup_wizard:completion.subtitle', "Congratulations! Your bakery management system is ready to use. Let's get started with your first tasks.")} {t('setup_wizard:completion.subtitle', "Your bakery management system is ready to use.")}
</p> </p>
</div> </div>
{/* Confetti Effect Placeholder */}
<div className="relative">
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="text-6xl opacity-10 animate-pulse">🎊🎉🎊</div>
</div>
</div>
{/* Next Steps */} {/* Next Steps */}
<div> <div className="text-left">
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-4 flex items-center gap-2"> <h2 className="text-xl font-semibold text-[var(--text-primary)] mb-4">
<svg className="w-6 h-6 text-[var(--color-primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
{t('setup_wizard:completion.next_steps', 'Recommended Next Steps')} {t('setup_wizard:completion.next_steps', 'Recommended Next Steps')}
</h2> </h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{nextSteps.map((step, index) => ( {nextSteps.map((step, index) => (
<div <div
key={index} key={index}
className="group bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg p-5 hover:border-[var(--color-primary)] hover:shadow-lg transition-all cursor-pointer" className="bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg p-4 hover:border-[var(--color-primary)] transition-colors cursor-pointer"
onClick={() => navigate(step.link)} onClick={() => navigate(step.link)}
> >
<div className="flex items-start gap-3 mb-3"> <div className="flex items-start gap-3">
<div className="flex-shrink-0 w-10 h-10 bg-gradient-to-br from-[var(--color-primary)]/10 to-[var(--color-primary)]/5 rounded-lg flex items-center justify-center text-[var(--color-primary)] group-hover:scale-110 transition-transform"> <div className="flex-shrink-0 w-10 h-10 bg-[var(--color-primary)]/10 rounded-lg flex items-center justify-center text-[var(--color-primary)]">
{step.icon} {step.icon}
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h3 className="font-semibold text-[var(--text-primary)] mb-1 group-hover:text-[var(--color-primary)] transition-colors"> <h3 className="font-semibold text-[var(--text-primary)] mb-1">
{step.title} {step.title}
</h3> </h3>
<p className="text-sm text-[var(--text-secondary)] leading-relaxed"> <p className="text-sm text-[var(--text-secondary)]">
{step.description} {step.description}
</p> </p>
</div> </div>
</div> </div>
<button className="text-sm font-medium text-[var(--color-primary)] hover:underline flex items-center gap-1"> </div>
{step.action} ))}
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> </div>
</div>
{/* Action Button */}
<div className="pt-4">
<button
onClick={handleGoToDashboard}
className="inline-flex items-center gap-2 px-8 py-3 bg-[var(--color-primary)] text-white font-semibold rounded-lg hover:bg-[var(--color-primary-700)] transition-colors"
>
{t('setup_wizard:completion.go_dashboard', 'Go to Dashboard')}
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg> </svg>
</button> </button>
</div> </div>
))}
</div>
</div>
{/* Pro Tips */}
<div>
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<svg className="w-6 h-6 text-[var(--color-warning)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
{t('setup_wizard:completion.tips', 'Pro Tips for Success')}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{tips.map((tip, index) => (
<div
key={index}
className="bg-gradient-to-br from-[var(--bg-secondary)] to-[var(--bg-primary)] border border-[var(--border-secondary)] rounded-lg p-4"
>
<div className="flex items-start gap-3">
<div className="text-3xl">{tip.icon}</div>
<div className="flex-1">
<h3 className="font-semibold text-[var(--text-primary)] mb-1">
{tip.title}
</h3>
<p className="text-sm text-[var(--text-secondary)] leading-relaxed">
{tip.description}
</p>
</div>
</div>
</div>
))}
</div>
</div>
{/* Quick Links */}
<div className="bg-[var(--color-info)]/10 border border-[var(--color-info)]/20 rounded-lg p-6">
<h3 className="font-semibold text-[var(--text-primary)] mb-3 flex items-center gap-2">
<svg className="w-5 h-5 text-[var(--color-info)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{t('setup_wizard:completion.need_help', 'Need Help?')}
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<button
onClick={() => navigate('/app/settings/bakery')}
className="flex items-center gap-2 p-3 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg hover:border-[var(--color-primary)] transition-colors text-left"
>
<svg className="w-5 h-5 text-[var(--text-secondary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<div className="flex-1 min-w-0">
<p className="font-medium text-sm text-[var(--text-primary)]">{t('setup_wizard:completion.settings', 'Settings')}</p>
<p className="text-xs text-[var(--text-tertiary)]">{t('setup_wizard:completion.settings_desc', 'Configure preferences')}</p>
</div>
</button>
<button
onClick={() => navigate('/app/dashboard')}
className="flex items-center gap-2 p-3 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg hover:border-[var(--color-primary)] transition-colors text-left"
>
<svg className="w-5 h-5 text-[var(--text-secondary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<div className="flex-1 min-w-0">
<p className="font-medium text-sm text-[var(--text-primary)]">{t('setup_wizard:completion.dashboard', 'Dashboard')}</p>
<p className="text-xs text-[var(--text-tertiary)]">{t('setup_wizard:completion.dashboard_desc', 'View overview')}</p>
</div>
</button>
<button
onClick={() => navigate('/app/operations/recipes')}
className="flex items-center gap-2 p-3 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg hover:border-[var(--color-primary)] transition-colors text-left"
>
<svg className="w-5 h-5 text-[var(--text-secondary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
<div className="flex-1 min-w-0">
<p className="font-medium text-sm text-[var(--text-primary)]">{t('setup_wizard:completion.recipes', 'Recipes')}</p>
<p className="text-xs text-[var(--text-tertiary)]">{t('setup_wizard:completion.recipes_desc', 'Manage recipes')}</p>
</div>
</button>
</div>
</div>
{/* Final CTA */}
<div className="text-center pt-4">
<button
onClick={handleGoToDashboard}
className="inline-flex items-center gap-2 px-8 py-4 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-success)] text-white font-semibold rounded-lg hover:shadow-lg transform hover:scale-105 transition-all"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
{t('setup_wizard:completion.go_dashboard', 'Go to Dashboard')}
</button>
<p className="text-sm text-[var(--text-tertiary)] mt-3">
{t('setup_wizard:completion.thanks', 'Thank you for completing the setup! Happy baking! 🥖🥐🍰')}
</p>
</div>
</div> </div>
); );
}; };

View File

@@ -1,10 +1,10 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SetupStepProps } from '../types'; import { SetupStepProps } from '../types';
import { useQualityTemplates, useCreateQualityTemplate } from '../../../../api/hooks/qualityTemplates'; import { useQualityTemplates, useCreateQualityTemplate, useUpdateQualityTemplate, useDeleteQualityTemplate } from '../../../../api/hooks/qualityTemplates';
import { useCurrentTenant } from '../../../../stores/tenant.store'; import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAuthUser } from '../../../../stores/auth.store'; import { useAuthUser } from '../../../../stores/auth.store';
import { QualityCheckType, ProcessStage, QualityCheckTemplate, QualityCheckTemplateCreate } from '../../../../api/types/qualityTemplates'; import { QualityCheckType, ProcessStage, QualityCheckTemplate } from '../../../../api/types/qualityTemplates';
export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplete, onPrevious, canContinue }) => { export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplete, onPrevious, canContinue }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -21,9 +21,12 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
// Mutations // Mutations
const createTemplateMutation = useCreateQualityTemplate(tenantId); const createTemplateMutation = useCreateQualityTemplate(tenantId);
const updateTemplateMutation = useUpdateQualityTemplate(tenantId);
const deleteTemplateMutation = useDeleteQualityTemplate(tenantId);
// Form state // Form state
const [isAdding, setIsAdding] = useState(false); const [isAdding, setIsAdding] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
check_type: QualityCheckType.VISUAL, check_type: QualityCheckType.VISUAL,
@@ -31,6 +34,14 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
applicable_stages: [] as ProcessStage[], applicable_stages: [] as ProcessStage[],
is_required: false, is_required: false,
is_critical: false, is_critical: false,
// Fields for MEASUREMENT, TEMPERATURE, WEIGHT
unit: '',
min_value: '',
max_value: '',
target_value: '',
tolerance_percentage: '',
// Fields for VISUAL
scoring_criteria: {} as Record<string, any>,
}); });
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
@@ -61,6 +72,36 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
newErrors.stages = t('setup_wizard:quality.errors.stages_required', 'At least one stage is required'); newErrors.stages = t('setup_wizard:quality.errors.stages_required', 'At least one stage is required');
} }
// Validate fields for MEASUREMENT, TEMPERATURE, WEIGHT
if ([QualityCheckType.MEASUREMENT, QualityCheckType.TEMPERATURE, QualityCheckType.WEIGHT].includes(formData.check_type)) {
if (!formData.unit.trim()) {
newErrors.unit = t('setup_wizard:quality.errors.unit_required', 'Unit is required for this check type');
}
const minVal = parseFloat(formData.min_value);
const maxVal = parseFloat(formData.max_value);
if (formData.min_value && isNaN(minVal)) {
newErrors.min_value = t('setup_wizard:quality.errors.invalid_number', 'Must be a valid number');
}
if (formData.max_value && isNaN(maxVal)) {
newErrors.max_value = t('setup_wizard:quality.errors.invalid_number', 'Must be a valid number');
}
if (formData.min_value && formData.max_value && minVal >= maxVal) {
newErrors.max_value = t('setup_wizard:quality.errors.max_greater_than_min', 'Maximum must be greater than minimum');
}
}
// Validate tolerance percentage if provided
if (formData.tolerance_percentage) {
const tolerance = parseFloat(formData.tolerance_percentage);
if (isNaN(tolerance) || tolerance < 0 || tolerance > 100) {
newErrors.tolerance_percentage = t('setup_wizard:quality.errors.tolerance_range', 'Must be between 0 and 100');
}
}
setErrors(newErrors); setErrors(newErrors);
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0;
}; };
@@ -72,7 +113,7 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
if (!validateForm()) return; if (!validateForm()) return;
try { try {
const templateData: QualityCheckTemplateCreate = { const templateData: any = {
name: formData.name, name: formData.name,
check_type: (formData.check_type as QualityCheckType) || QualityCheckType.VISUAL, check_type: (formData.check_type as QualityCheckType) || QualityCheckType.VISUAL,
description: formData.description || undefined, description: formData.description || undefined,
@@ -81,10 +122,37 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
is_critical: formData.is_critical, is_critical: formData.is_critical,
is_active: true, is_active: true,
weight: formData.is_critical ? 10 : 5, weight: formData.is_critical ? 10 : 5,
created_by: userId || '',
}; };
// Only add created_by for new templates
if (!editingId) {
templateData.created_by = userId || '';
}
// Add measurement-related fields for MEASUREMENT, TEMPERATURE, WEIGHT
if ([QualityCheckType.MEASUREMENT, QualityCheckType.TEMPERATURE, QualityCheckType.WEIGHT].includes(formData.check_type)) {
templateData.unit = formData.unit;
if (formData.min_value) templateData.min_value = parseFloat(formData.min_value);
if (formData.max_value) templateData.max_value = parseFloat(formData.max_value);
if (formData.target_value) templateData.target_value = parseFloat(formData.target_value);
if (formData.tolerance_percentage) templateData.tolerance_percentage = parseFloat(formData.tolerance_percentage);
}
// Add scoring_criteria for VISUAL checks
if (formData.check_type === QualityCheckType.VISUAL && Object.keys(formData.scoring_criteria).length > 0) {
templateData.scoring_criteria = formData.scoring_criteria;
}
if (editingId) {
// Update existing template
await updateTemplateMutation.mutateAsync({
templateId: editingId,
templateData,
});
} else {
// Create new template
await createTemplateMutation.mutateAsync(templateData); await createTemplateMutation.mutateAsync(templateData);
}
// Reset form // Reset form
resetForm(); resetForm();
@@ -101,16 +169,47 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
applicable_stages: [], applicable_stages: [],
is_required: false, is_required: false,
is_critical: false, is_critical: false,
unit: '',
min_value: '',
max_value: '',
target_value: '',
tolerance_percentage: '',
scoring_criteria: {},
}); });
setErrors({}); setErrors({});
setIsAdding(false); setIsAdding(false);
setEditingId(null);
}; };
const toggleStage = (stage: ProcessStage) => { const handleEdit = (template: QualityCheckTemplate) => {
const stages = formData.applicable_stages.includes(stage) setFormData({
? formData.applicable_stages.filter((s) => s !== stage) name: template.name,
: [...formData.applicable_stages, stage]; check_type: template.check_type,
setFormData({ ...formData, applicable_stages: stages }); description: template.description || '',
applicable_stages: template.applicable_stages || [],
is_required: template.is_required,
is_critical: template.is_critical,
unit: template.unit || '',
min_value: template.min_value?.toString() || '',
max_value: template.max_value?.toString() || '',
target_value: template.target_value?.toString() || '',
tolerance_percentage: template.tolerance_percentage?.toString() || '',
scoring_criteria: template.scoring_criteria || {},
});
setEditingId(template.id);
setIsAdding(true);
};
const handleDelete = async (templateId: string) => {
if (!window.confirm(t('setup_wizard:quality.confirm_delete', 'Are you sure you want to delete this quality check?'))) {
return;
}
try {
await deleteTemplateMutation.mutateAsync(templateId);
} catch (error) {
console.error('Error deleting quality template:', error);
}
}; };
const checkTypeOptions = [ const checkTypeOptions = [
@@ -218,6 +317,29 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
)} )}
</div> </div>
</div> </div>
<div className="flex items-center gap-1 ml-2">
<button
type="button"
onClick={() => handleEdit(template)}
className="p-1.5 text-[var(--text-secondary)] hover:text-[var(--color-primary)] hover:bg-[var(--bg-primary)] rounded transition-colors"
aria-label={t('common:edit', 'Edit')}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
type="button"
onClick={() => handleDelete(template.id)}
className="p-1.5 text-[var(--text-secondary)] hover:text-[var(--color-error)] hover:bg-[var(--color-error)]/10 rounded transition-colors"
aria-label={t('common:delete', 'Delete')}
disabled={deleteTemplateMutation.isPending}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div> </div>
))} ))}
</div> </div>
@@ -229,7 +351,10 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
<form onSubmit={handleSubmit} className="space-y-4 p-4 border-2 border-[var(--color-primary)] rounded-lg bg-[var(--bg-secondary)]"> <form onSubmit={handleSubmit} className="space-y-4 p-4 border-2 border-[var(--color-primary)] rounded-lg bg-[var(--bg-secondary)]">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-[var(--text-primary)]"> <h4 className="font-medium text-[var(--text-primary)]">
{t('setup_wizard:quality.add_check', 'Add Quality Check')} {editingId
? t('setup_wizard:quality.edit_check', 'Edit Quality Check')
: t('setup_wizard:quality.add_check', 'Add Quality Check')
}
</h4> </h4>
<button <button
type="button" type="button"
@@ -300,6 +425,115 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
/> />
</div> </div>
{/* Conditional fields for MEASUREMENT, TEMPERATURE, WEIGHT */}
{[QualityCheckType.MEASUREMENT, QualityCheckType.TEMPERATURE, QualityCheckType.WEIGHT].includes(formData.check_type) && (
<div className="space-y-4 p-4 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded-lg">
<h5 className="text-sm font-semibold text-[var(--text-primary)] flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
{t('setup_wizard:quality.measurement_settings', 'Measurement Settings')}
</h5>
{/* Unit */}
<div>
<label htmlFor="unit" className="block text-sm font-medium text-[var(--text-primary)] mb-1">
{t('setup_wizard:quality.fields.unit', 'Unit')} <span className="text-[var(--color-error)]">*</span>
</label>
<input
id="unit"
type="text"
value={formData.unit}
onChange={(e) => setFormData({ ...formData, unit: e.target.value })}
className={`w-full px-3 py-2 bg-[var(--bg-primary)] border ${errors.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)]`}
placeholder={
formData.check_type === QualityCheckType.TEMPERATURE
? t('setup_wizard:quality.placeholders.unit_temp', 'e.g., °C, °F')
: formData.check_type === QualityCheckType.WEIGHT
? t('setup_wizard:quality.placeholders.unit_weight', 'e.g., g, kg, oz, lb')
: t('setup_wizard:quality.placeholders.unit_measurement', 'e.g., cm, mm, inches')
}
/>
{errors.unit && <p className="mt-1 text-xs text-[var(--color-error)]">{errors.unit}</p>}
</div>
{/* Min and Max Values */}
<div className="grid grid-cols-2 gap-3">
<div>
<label htmlFor="min_value" className="block text-sm font-medium text-[var(--text-primary)] mb-1">
{t('setup_wizard:quality.fields.min_value', 'Minimum Value')}
</label>
<input
id="min_value"
type="number"
step="any"
value={formData.min_value}
onChange={(e) => setFormData({ ...formData, min_value: e.target.value })}
className={`w-full px-3 py-2 bg-[var(--bg-primary)] border ${errors.min_value ? 'border-[var(--color-error)]' : 'border-[var(--border-secondary)]'} rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]`}
placeholder="0"
/>
{errors.min_value && <p className="mt-1 text-xs text-[var(--color-error)]">{errors.min_value}</p>}
</div>
<div>
<label htmlFor="max_value" className="block text-sm font-medium text-[var(--text-primary)] mb-1">
{t('setup_wizard:quality.fields.max_value', 'Maximum Value')}
</label>
<input
id="max_value"
type="number"
step="any"
value={formData.max_value}
onChange={(e) => setFormData({ ...formData, max_value: e.target.value })}
className={`w-full px-3 py-2 bg-[var(--bg-primary)] border ${errors.max_value ? 'border-[var(--color-error)]' : 'border-[var(--border-secondary)]'} rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]`}
placeholder="100"
/>
{errors.max_value && <p className="mt-1 text-xs text-[var(--color-error)]">{errors.max_value}</p>}
</div>
</div>
{/* Target Value and Tolerance */}
<div className="grid grid-cols-2 gap-3">
<div>
<label htmlFor="target_value" className="block text-sm font-medium text-[var(--text-primary)] mb-1">
{t('setup_wizard:quality.fields.target_value', 'Target Value')}
</label>
<input
id="target_value"
type="number"
step="any"
value={formData.target_value}
onChange={(e) => setFormData({ ...formData, target_value: e.target.value })}
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)]"
placeholder={t('setup_wizard:quality.placeholders.target', 'Optional')}
/>
</div>
<div>
<label htmlFor="tolerance_percentage" className="block text-sm font-medium text-[var(--text-primary)] mb-1">
{t('setup_wizard:quality.fields.tolerance', 'Tolerance %')}
</label>
<input
id="tolerance_percentage"
type="number"
step="0.1"
min="0"
max="100"
value={formData.tolerance_percentage}
onChange={(e) => setFormData({ ...formData, tolerance_percentage: e.target.value })}
className={`w-full px-3 py-2 bg-[var(--bg-primary)] border ${errors.tolerance_percentage ? 'border-[var(--color-error)]' : 'border-[var(--border-secondary)]'} rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]`}
placeholder="5"
/>
{errors.tolerance_percentage && <p className="mt-1 text-xs text-[var(--color-error)]">{errors.tolerance_percentage}</p>}
</div>
</div>
<p className="text-xs text-[var(--text-secondary)] italic">
{t('setup_wizard:quality.measurement_help', 'Define the acceptable range and target for this measurement.')}
</p>
</div>
)}
{/* Applicable Stages */} {/* Applicable Stages */}
<div> <div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2"> <label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
@@ -370,10 +604,10 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
<div className="flex gap-2 pt-2"> <div className="flex gap-2 pt-2">
<button <button
type="submit" type="submit"
disabled={createTemplateMutation.isPending || !userId} disabled={createTemplateMutation.isPending || updateTemplateMutation.isPending || !userId}
className="px-4 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" className="px-4 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"
> >
{createTemplateMutation.isPending ? ( {(createTemplateMutation.isPending || updateTemplateMutation.isPending) ? (
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24"> <svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /> <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
@@ -381,6 +615,8 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
</svg> </svg>
{t('common:saving', 'Saving...')} {t('common:saving', 'Saving...')}
</span> </span>
) : editingId ? (
t('common:update', 'Update')
) : ( ) : (
t('common:add', 'Add') t('common:add', 'Add')
)} )}

View File

@@ -38,6 +38,7 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
// Form state // Form state
const [isAdding, setIsAdding] = useState(false); const [isAdding, setIsAdding] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
description: '', description: '',
@@ -108,6 +109,30 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
if (!validateForm()) return; if (!validateForm()) return;
try { try {
if (editingId) {
// Update existing recipe
const recipeData: RecipeCreate = {
name: formData.name,
description: formData.description || undefined,
finished_product_id: formData.finished_product_id,
yield_quantity: Number(formData.yield_quantity),
yield_unit: formData.yield_unit,
category: formData.category || undefined,
ingredients: recipeIngredients.map((ing) => ({
ingredient_id: ing.ingredient_id,
quantity: Number(ing.quantity),
unit: ing.unit,
ingredient_order: ing.ingredient_order,
is_optional: false,
} as RecipeIngredientCreate)),
};
await updateRecipeMutation.mutateAsync({
id: editingId,
data: recipeData,
});
} else {
// Create new recipe
const recipeData: RecipeCreate = { const recipeData: RecipeCreate = {
name: formData.name, name: formData.name,
description: formData.description || undefined, description: formData.description || undefined,
@@ -125,6 +150,7 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
}; };
await createRecipeMutation.mutateAsync(recipeData); await createRecipeMutation.mutateAsync(recipeData);
}
// Reset form // Reset form
resetForm(); resetForm();
@@ -145,6 +171,43 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
setRecipeIngredients([]); setRecipeIngredients([]);
setErrors({}); setErrors({});
setIsAdding(false); setIsAdding(false);
setEditingId(null);
};
const handleEdit = async (recipe: RecipeResponse) => {
// If the recipe doesn't have ingredients loaded, fetch the full recipe
if (!recipe.ingredients || recipe.ingredients.length === 0) {
try {
const { recipesService } = await import('../../../../api');
const fullRecipe = await recipesService.getRecipe(tenantId, recipe.id);
recipe = fullRecipe;
} catch (error) {
console.error('Error fetching full recipe:', error);
}
}
// Populate form with recipe data
setFormData({
name: recipe.name,
description: recipe.description || '',
finished_product_id: recipe.finished_product_id,
yield_quantity: recipe.yield_quantity.toString(),
yield_unit: recipe.yield_unit as MeasurementUnit,
category: recipe.category || '',
});
// Populate recipe ingredients
const ingredients = (recipe.ingredients || []).map((ing, index) => ({
ingredient_id: ing.ingredient_id,
quantity: ing.quantity.toString(),
unit: ing.unit as MeasurementUnit,
ingredient_order: ing.ingredient_order || index + 1,
}));
setRecipeIngredients(ingredients);
setEditingId(recipe.id);
setIsAdding(true);
}; };
const handleDelete = async (recipeId: string) => { const handleDelete = async (recipeId: string) => {
@@ -246,15 +309,15 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
}; };
const unitOptions = [ const unitOptions = [
{ value: MeasurementUnit.GRAMS, label: t('recipes:unit.g', 'Grams (g)') }, { value: MeasurementUnit.GRAMS, label: t('recipes:units.g', 'Grams (g)') },
{ value: MeasurementUnit.KILOGRAMS, label: t('recipes:unit.kg', 'Kilograms (kg)') }, { value: MeasurementUnit.KILOGRAMS, label: t('recipes:units.kg', 'Kilograms (kg)') },
{ value: MeasurementUnit.MILLILITERS, label: t('recipes:unit.ml', 'Milliliters (ml)') }, { value: MeasurementUnit.MILLILITERS, label: t('recipes:units.ml', 'Milliliters (ml)') },
{ value: MeasurementUnit.LITERS, label: t('recipes:unit.l', 'Liters (l)') }, { value: MeasurementUnit.LITERS, label: t('recipes:units.l', 'Liters (l)') },
{ value: MeasurementUnit.UNITS, label: t('recipes:unit.units', 'Units') }, { value: MeasurementUnit.UNITS, label: t('recipes:units.units', 'Units') },
{ value: MeasurementUnit.PIECES, label: t('recipes:unit.pieces', 'Pieces') }, { value: MeasurementUnit.PIECES, label: t('recipes:units.pieces', 'Pieces') },
{ value: MeasurementUnit.CUPS, label: t('recipes:unit.cups', 'Cups') }, { value: MeasurementUnit.CUPS, label: t('recipes:units.cups', 'Cups') },
{ value: MeasurementUnit.TABLESPOONS, label: t('recipes:unit.tbsp', 'Tablespoons') }, { value: MeasurementUnit.TABLESPOONS, label: t('recipes:units.tbsp', 'Tablespoons') },
{ value: MeasurementUnit.TEASPOONS, label: t('recipes:unit.tsp', 'Teaspoons') }, { value: MeasurementUnit.TEASPOONS, label: t('recipes:units.tsp', 'Teaspoons') },
]; ];
return ( return (
@@ -354,19 +417,26 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
))} ))}
</ul> </ul>
</div> </div>
{template.instructions && ( {(template.instructions || template.instructionsKey) && (
<div> <div>
<p className="font-medium text-[var(--text-primary)] mb-1">{t('setup_wizard:recipes.template_instructions', 'Instructions:')}</p> <p className="font-medium text-[var(--text-primary)] mb-1">{t('setup_wizard:recipes.template_instructions', 'Instructions:')}</p>
<p className="text-[var(--text-secondary)] whitespace-pre-line">{template.instructions}</p> <p className="text-[var(--text-secondary)] whitespace-pre-line">
{template.instructionsKey ? t(template.instructionsKey, template.instructions || '') : template.instructions}
</p>
</div> </div>
)} )}
{template.tips && template.tips.length > 0 && ( {((template.tips && template.tips.length > 0) || (template.tipsKeys && template.tipsKeys.length > 0)) && (
<div> <div>
<p className="font-medium text-[var(--text-primary)] mb-1">{t('setup_wizard:recipes.template_tips', 'Tips:')}</p> <p className="font-medium text-[var(--text-primary)] mb-1">{t('setup_wizard:recipes.template_tips', 'Tips:')}</p>
<ul className="list-disc list-inside space-y-0.5 text-[var(--text-secondary)]"> <ul className="list-disc list-inside space-y-0.5 text-[var(--text-secondary)]">
{template.tips.map((tip, idx) => ( {template.tipsKeys
? template.tipsKeys.map((tipKey, idx) => (
<li key={idx}>{t(tipKey, template.tips?.[idx] || '')}</li>
))
: template.tips?.map((tip, idx) => (
<li key={idx}>{tip}</li> <li key={idx}>{tip}</li>
))} ))
}
</ul> </ul>
</div> </div>
)} )}
@@ -446,7 +516,9 @@ 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" /> <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> </svg>
<span className="text-sm font-medium text-[var(--text-primary)]"> <span className="text-sm font-medium text-[var(--text-primary)]">
{t('setup_wizard:recipes.added_count', { count: recipes.length, defaultValue: '{count} recipe added' })} {recipes.length === 1
? t('setup_wizard:recipes.added_count', { count: recipes.length })
: t('setup_wizard:recipes.added_count_plural', { count: recipes.length })}
</span> </span>
</div> </div>
{recipes.length >= 1 && ( {recipes.length >= 1 && (
@@ -495,6 +567,16 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
</div> </div>
</div> </div>
<div className="flex items-center gap-2 ml-4"> <div className="flex items-center gap-2 ml-4">
<button
type="button"
onClick={() => handleEdit(recipe)}
className="p-1.5 text-[var(--text-secondary)] hover:text-[var(--color-primary)] hover:bg-[var(--bg-primary)] rounded transition-colors"
aria-label={t('common:edit', 'Edit')}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button <button
type="button" type="button"
onClick={() => handleDelete(recipe.id)} onClick={() => handleDelete(recipe.id)}
@@ -518,7 +600,7 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
<form onSubmit={handleSubmit} className="space-y-4 p-4 border-2 border-[var(--color-primary)] rounded-lg bg-[var(--bg-secondary)]"> <form onSubmit={handleSubmit} className="space-y-4 p-4 border-2 border-[var(--color-primary)] rounded-lg bg-[var(--bg-secondary)]">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-[var(--text-primary)]"> <h4 className="font-medium text-[var(--text-primary)]">
{t('setup_wizard:recipes.add_recipe', 'Add Recipe')} {editingId ? t('setup_wizard:recipes.edit_recipe', 'Edit Recipe') : t('setup_wizard:recipes.add_recipe', 'Add Recipe')}
</h4> </h4>
<button <button
type="button" type="button"
@@ -679,7 +761,7 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
> >
{unitOptions.map((option) => ( {unitOptions.map((option) => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
{option.value} {option.label}
</option> </option>
))} ))}
</select> </select>
@@ -708,10 +790,10 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
<div className="flex gap-2 pt-2"> <div className="flex gap-2 pt-2">
<button <button
type="submit" type="submit"
disabled={createRecipeMutation.isPending} disabled={createRecipeMutation.isPending || updateRecipeMutation.isPending}
className="px-4 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" className="px-4 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"
> >
{createRecipeMutation.isPending ? ( {(createRecipeMutation.isPending || updateRecipeMutation.isPending) ? (
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24"> <svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /> <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
@@ -719,6 +801,8 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
</svg> </svg>
{t('common:saving', 'Saving...')} {t('common:saving', 'Saving...')}
</span> </span>
) : editingId ? (
t('common:update', 'Update')
) : ( ) : (
t('common:add', 'Add') t('common:add', 'Add')
)} )}

View File

@@ -213,7 +213,7 @@ export const SupplierProductManager: React.FC<SupplierProductManagerProps> = ({
<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" /> <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> </svg>
<span className="text-sm font-medium text-[var(--text-primary)]"> <span className="text-sm font-medium text-[var(--text-primary)]">
{t('setup_wizard:suppliers.products_for', 'Products for {{name}}', { name: supplierName })} {t('setup_wizard:suppliers.products_for', 'Products for {name}', { name: supplierName })}
</span> </span>
</div> </div>
<button <button

View File

@@ -69,7 +69,9 @@
"expand": "Expand", "expand": "Expand",
"collapse": "Collapse", "collapse": "Collapse",
"save_draft": "Save Draft", "save_draft": "Save Draft",
"confirm_receipt": "Confirm Receipt" "confirm_receipt": "Confirm Receipt",
"hide": "Hide",
"preview": "Preview"
}, },
"saved": "saved", "saved": "saved",
"item": "item", "item": "item",

View File

@@ -0,0 +1,7 @@
{
"bizcochuelo": {
"instructions": "1. Beat eggs with sugar until fluffy\n2. Add vanilla\n3. Gently fold in flour and baking powder\n4. Pour into greased pan\n5. Bake at 180°C for 35 minutes",
"tip1": "Do not overmix after adding flour",
"tip2": "Test doneness with toothpick"
}
}

View File

@@ -147,7 +147,7 @@
"prerequisites_desc": "You need at least 2 ingredients in your inventory before creating recipes. Go back to the Inventory step to add more ingredients.", "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": "{count} recipe added",
"added_count_plural": "{count} recipes added", "added_count_plural": "{count} recipes added",
"minimum_met": "{count} recipe(s) added - Ready to continue!", "minimum_met": "Ready to continue!",
"your_recipes": "Your Recipes", "your_recipes": "Your Recipes",
"yield_label": "Yield", "yield_label": "Yield",
"add_recipe": "Add Recipe", "add_recipe": "Add Recipe",
@@ -189,23 +189,40 @@
"recommended": "2+ recommended (optional)", "recommended": "2+ recommended (optional)",
"your_checks": "Your Quality Checks", "your_checks": "Your Quality Checks",
"add_check": "Add Quality Check", "add_check": "Add Quality Check",
"edit_check": "Edit Quality Check",
"confirm_delete": "Are you sure you want to delete this quality check?",
"add_first": "Add Your First Quality Check", "add_first": "Add Your First Quality Check",
"add_another": "Add Another Quality Check", "add_another": "Add Another Quality Check",
"measurement_settings": "Measurement Settings",
"measurement_help": "Define the acceptable range and target for this measurement.",
"fields": { "fields": {
"name": "Check Name", "name": "Check Name",
"check_type": "Check Type", "check_type": "Check Type",
"description": "Description", "description": "Description",
"stages": "Applicable Stages", "stages": "Applicable Stages",
"required": "Required check (must be completed)", "required": "Required check (must be completed)",
"critical": "Critical check (failure stops production)" "critical": "Critical check (failure stops production)",
"unit": "Unit",
"min_value": "Minimum Value",
"max_value": "Maximum Value",
"target_value": "Target Value",
"tolerance": "Tolerance %"
}, },
"placeholders": { "placeholders": {
"name": "e.g., Crust color check, Dough temperature", "name": "e.g., Crust color check, Dough temperature",
"description": "What should be checked and why..." "description": "What should be checked and why...",
"unit_temp": "e.g., °C, °F",
"unit_weight": "e.g., g, kg, oz, lb",
"unit_measurement": "e.g., cm, mm, inches",
"target": "Optional"
}, },
"errors": { "errors": {
"name_required": "Name is required", "name_required": "Name is required",
"stages_required": "At least one stage is required" "stages_required": "At least one stage is required",
"unit_required": "Unit is required for this check type",
"invalid_number": "Must be a valid number",
"max_greater_than_min": "Maximum must be greater than minimum",
"tolerance_range": "Must be between 0 and 100"
} }
}, },
"team": { "team": {
@@ -272,6 +289,31 @@
"completion": { "completion": {
"title": "🎉 Setup Complete!", "title": "🎉 Setup Complete!",
"subtitle": "Congratulations! Your bakery management system is ready to use. Let's get started with your first tasks.", "subtitle": "Congratulations! Your bakery management system is ready to use. Let's get started with your first tasks.",
"congratulations": "Congratulations! Your System Is Ready",
"all_configured": "You have successfully configured {name} with our intelligent management system. Everything is ready to start optimizing your bakery.",
"what_configured": "What You Have Configured",
"bakery_info": "Bakery Information",
"inventory_ai": "AI Inventory",
"suppliers_added": "Suppliers Added",
"recipes_configured": "Recipes Configured",
"quality_set": "Quality Standards",
"team_invited": "Team Members",
"quick": {
"analytics": "Analytics",
"inventory": "Inventory",
"procurement": "Purchases",
"production": "Production"
},
"tips_title": "Tips to Maximize Your Success",
"tip1": "Review the dashboard daily for insights",
"tip2": "Update inventory regularly",
"tip3": "Use AI predictions for planning",
"tip4": "Invite your team to collaborate",
"go_to_dashboard": "Start Using the System",
"need_help": "Need help? Visit our",
"user_guide": "user guide",
"or_contact": "or contact our",
"support_team": "support team",
"next_steps": "Recommended Next Steps", "next_steps": "Recommended Next Steps",
"step1_title": "Start Production", "step1_title": "Start Production",
"step1_desc": "Create your first production batch using your configured recipes", "step1_desc": "Create your first production batch using your configured recipes",
@@ -291,7 +333,6 @@
"tip3_desc": "Check your production analytics every week to optimize recipes and reduce waste", "tip3_desc": "Check your production analytics every week to optimize recipes and reduce waste",
"tip4_title": "Maintain Supplier Relationships", "tip4_title": "Maintain Supplier Relationships",
"tip4_desc": "Keep supplier information current and track order performance for better partnerships", "tip4_desc": "Keep supplier information current and track order performance for better partnerships",
"need_help": "Need Help?",
"settings": "Settings", "settings": "Settings",
"settings_desc": "Configure preferences", "settings_desc": "Configure preferences",
"dashboard": "Dashboard", "dashboard": "Dashboard",

View File

@@ -69,7 +69,9 @@
"expand": "Expandir", "expand": "Expandir",
"collapse": "Contraer", "collapse": "Contraer",
"save_draft": "Guardar Borrador", "save_draft": "Guardar Borrador",
"confirm_receipt": "Confirmar Recepción" "confirm_receipt": "Confirmar Recepción",
"hide": "Ocultar",
"preview": "Vista previa"
}, },
"saved": "ahorrado", "saved": "ahorrado",
"item": "artículo", "item": "artículo",

View File

@@ -0,0 +1,7 @@
{
"bizcochuelo": {
"instructions": "1. Batir los huevos con el azúcar hasta que estén esponjosos\n2. Agregar la vainilla\n3. Incorporar suavemente la harina y el polvo de hornear\n4. Verter en molde enmantecado\n5. Hornear a 180°C durante 35 minutos",
"tip1": "No mezclar demasiado después de agregar la harina",
"tip2": "Probar la cocción con un palillo"
}
}

View File

@@ -147,7 +147,7 @@
"prerequisites_desc": "Necesitas al menos 2 ingredientes en tu inventario antes de crear recetas. Regresa al paso de Inventario para agregar 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": "{count} receta agregada",
"added_count_plural": "{count} recetas agregadas", "added_count_plural": "{count} recetas agregadas",
"minimum_met": "{count} receta(s) agregada(s) - ¡Listo para continuar!", "minimum_met": "¡Listo para continuar!",
"your_recipes": "Tus Recetas", "your_recipes": "Tus Recetas",
"yield_label": "Rendimiento", "yield_label": "Rendimiento",
"add_recipe": "Agregar Receta", "add_recipe": "Agregar Receta",
@@ -189,23 +189,40 @@
"recommended": "2+ recomendados (opcional)", "recommended": "2+ recomendados (opcional)",
"your_checks": "Tus Controles de Calidad", "your_checks": "Tus Controles de Calidad",
"add_check": "Agregar Control de Calidad", "add_check": "Agregar Control de Calidad",
"edit_check": "Editar Control de Calidad",
"confirm_delete": "¿Estás seguro de que quieres eliminar este control de calidad?",
"add_first": "Agrega tu Primer Control de Calidad", "add_first": "Agrega tu Primer Control de Calidad",
"add_another": "Agregar Otro Control de Calidad", "add_another": "Agregar Otro Control de Calidad",
"measurement_settings": "Configuración de Medición",
"measurement_help": "Define el rango aceptable y el objetivo para esta medición.",
"fields": { "fields": {
"name": "Nombre del Control", "name": "Nombre del Control",
"check_type": "Tipo de Control", "check_type": "Tipo de Control",
"description": "Descripción", "description": "Descripción",
"stages": "Etapas Aplicables", "stages": "Etapas Aplicables",
"required": "Control obligatorio (debe completarse)", "required": "Control obligatorio (debe completarse)",
"critical": "Control crítico (el fallo detiene la producción)" "critical": "Control crítico (el fallo detiene la producción)",
"unit": "Unidad",
"min_value": "Valor Mínimo",
"max_value": "Valor Máximo",
"target_value": "Valor Objetivo",
"tolerance": "Tolerancia %"
}, },
"placeholders": { "placeholders": {
"name": "ej., Control de color de corteza, Temperatura de masa", "name": "ej., Control de color de corteza, Temperatura de masa",
"description": "Qué debe verificarse y por qué..." "description": "Qué debe verificarse y por qué...",
"unit_temp": "ej., °C, °F",
"unit_weight": "ej., g, kg, oz, lb",
"unit_measurement": "ej., cm, mm, pulgadas",
"target": "Opcional"
}, },
"errors": { "errors": {
"name_required": "El nombre es obligatorio", "name_required": "El nombre es obligatorio",
"stages_required": "Se requiere al menos una etapa" "stages_required": "Se requiere al menos una etapa",
"unit_required": "La unidad es obligatoria para este tipo de control",
"invalid_number": "Debe ser un número válido",
"max_greater_than_min": "El máximo debe ser mayor que el mínimo",
"tolerance_range": "Debe estar entre 0 y 100"
} }
}, },
"team": { "team": {
@@ -272,6 +289,31 @@
"completion": { "completion": {
"title": "🎉 ¡Configuración Completa!", "title": "🎉 ¡Configuración Completa!",
"subtitle": "¡Felicitaciones! Tu sistema de gestión de panadería está listo para usar. Comencemos con tus primeras tareas.", "subtitle": "¡Felicitaciones! Tu sistema de gestión de panadería está listo para usar. Comencemos con tus primeras tareas.",
"congratulations": "¡Felicidades! Tu Sistema Está Listo",
"all_configured": "Has configurado exitosamente {name} con nuestro sistema de gestión inteligente. Todo está listo para empezar a optimizar tu panadería.",
"what_configured": "Lo Que Has Configurado",
"bakery_info": "Información de Panadería",
"inventory_ai": "Inventario con IA",
"suppliers_added": "Proveedores Agregados",
"recipes_configured": "Recetas Configuradas",
"quality_set": "Estándares de Calidad",
"team_invited": "Miembros del Equipo",
"quick": {
"analytics": "Analíticas",
"inventory": "Inventario",
"procurement": "Compras",
"production": "Producción"
},
"tips_title": "Consejos para Maximizar tu Éxito",
"tip1": "Revisa el dashboard diariamente para obtener información",
"tip2": "Actualiza el inventario regularmente",
"tip3": "Usa las predicciones de IA para planificar",
"tip4": "Invita a tu equipo para colaborar",
"go_to_dashboard": "Comenzar a Usar el Sistema",
"need_help": "¿Necesitas ayuda? Visita nuestra",
"user_guide": "guía de usuario",
"or_contact": "o contacta a nuestro",
"support_team": "equipo de soporte",
"next_steps": "Próximos Pasos Recomendados", "next_steps": "Próximos Pasos Recomendados",
"step1_title": "Iniciar Producción", "step1_title": "Iniciar Producción",
"step1_desc": "Crea tu primer lote de producción usando tus recetas configuradas", "step1_desc": "Crea tu primer lote de producción usando tus recetas configuradas",
@@ -291,7 +333,6 @@
"tip3_desc": "Revisa tus analíticas de producción cada semana para optimizar recetas y reducir desperdicios", "tip3_desc": "Revisa tus analíticas de producción cada semana para optimizar recetas y reducir desperdicios",
"tip4_title": "Mantén las Relaciones con Proveedores", "tip4_title": "Mantén las Relaciones con Proveedores",
"tip4_desc": "Mantén la información de proveedores actualizada y rastrea el rendimiento de pedidos para mejores asociaciones", "tip4_desc": "Mantén la información de proveedores actualizada y rastrea el rendimiento de pedidos para mejores asociaciones",
"need_help": "¿Necesitas Ayuda?",
"settings": "Configuración", "settings": "Configuración",
"settings_desc": "Configurar preferencias", "settings_desc": "Configurar preferencias",
"dashboard": "Panel", "dashboard": "Panel",

View File

@@ -67,7 +67,9 @@
"expand": "Zabaldu", "expand": "Zabaldu",
"collapse": "Tolestu", "collapse": "Tolestu",
"save_draft": "Zirriborroa Gorde", "save_draft": "Zirriborroa Gorde",
"confirm_receipt": "Harrera Berretsi" "confirm_receipt": "Harrera Berretsi",
"hide": "Ezkutatu",
"preview": "Aurrebista"
}, },
"saved": "aurreztuta", "saved": "aurreztuta",
"item": "produktua", "item": "produktua",

View File

@@ -0,0 +1,7 @@
{
"bizcochuelo": {
"instructions": "1. Arrautzak azukrearekin irabiatu esponjatsu egon arte\n2. Bainilla gehitu\n3. Irina eta hornogatz hautsa pixkanaka gehitu\n4. Irin-mantekadun moldean bota\n5. 180°C-tan 35 minutuz labean egosi",
"tip1": "Ez nahastu gehiegi irina gehitu ondoren",
"tip2": "Egosketa hagaxka batekin probatu"
}
}

View File

@@ -147,7 +147,7 @@
"prerequisites_desc": "Gutxienez 2 osagai behar dituzu zure inbentarioan errezetak sortu aurretik. Itzuli Inbentario urratsera osagai gehiago gehitzeko.", "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": "Errezeta {count} gehituta",
"added_count_plural": "{count} errezeta gehituta", "added_count_plural": "{count} errezeta gehituta",
"minimum_met": "{count} errezeta gehituta - Jarraitzeko prest!", "minimum_met": "Jarraitzeko prest!",
"your_recipes": "Zure Errezetak", "your_recipes": "Zure Errezetak",
"yield_label": "Etekin", "yield_label": "Etekin",
"add_recipe": "Errezeta Gehitu", "add_recipe": "Errezeta Gehitu",
@@ -189,23 +189,40 @@
"recommended": "2+ gomendatzen dira (aukerakoa)", "recommended": "2+ gomendatzen dira (aukerakoa)",
"your_checks": "Zure Kalitate Kontrolak", "your_checks": "Zure Kalitate Kontrolak",
"add_check": "Kalitate Kontrola Gehitu", "add_check": "Kalitate Kontrola Gehitu",
"edit_check": "Kalitate Kontrola Editatu",
"confirm_delete": "Ziur zaude kalitate kontrol hau ezabatu nahi duzula?",
"add_first": "Gehitu Zure Lehen Kalitate Kontrola", "add_first": "Gehitu Zure Lehen Kalitate Kontrola",
"add_another": "Beste Kalitate Kontrol Bat Gehitu", "add_another": "Beste Kalitate Kontrol Bat Gehitu",
"measurement_settings": "Neurketa Ezarpenak",
"measurement_help": "Definitu neurketa honetarako tartea eta helburu onargarria.",
"fields": { "fields": {
"name": "Kontrolaren Izena", "name": "Kontrolaren Izena",
"check_type": "Kontrol Mota", "check_type": "Kontrol Mota",
"description": "Deskribapena", "description": "Deskribapena",
"stages": "Etapa Aplikagarriak", "stages": "Etapa Aplikagarriak",
"required": "Nahitaezko kontrola (osatu behar da)", "required": "Nahitaezko kontrola (osatu behar da)",
"critical": "Kontrol kritikoa (hutsegiteak ekoizpena gelditzen du)" "critical": "Kontrol kritikoa (hutsegiteak ekoizpena gelditzen du)",
"unit": "Unitatea",
"min_value": "Balio Minimoa",
"max_value": "Balio Maximoa",
"target_value": "Helburu Balioa",
"tolerance": "Tolerantzia %"
}, },
"placeholders": { "placeholders": {
"name": "adib., Azal kolorearen kontrola, Oraren tenperatura", "name": "adib., Azal kolorearen kontrola, Oraren tenperatura",
"description": "Zer egiaztatu behar den eta zergatik..." "description": "Zer egiaztatu behar den eta zergatik...",
"unit_temp": "adib., °C, °F",
"unit_weight": "adib., g, kg, oz, lb",
"unit_measurement": "adib., cm, mm, hazbete",
"target": "Aukerakoa"
}, },
"errors": { "errors": {
"name_required": "Izena beharrezkoa da", "name_required": "Izena beharrezkoa da",
"stages_required": "Gutxienez etapa bat beharrezkoa da" "stages_required": "Gutxienez etapa bat beharrezkoa da",
"unit_required": "Unitatea beharrezkoa da kontrol mota honetarako",
"invalid_number": "Zenbaki baliozkoa izan behar da",
"max_greater_than_min": "Maximoak minimoa baino handiagoa izan behar du",
"tolerance_range": "0 eta 100 artean egon behar da"
} }
}, },
"team": { "team": {
@@ -272,6 +289,31 @@
"completion": { "completion": {
"title": "🎉 Konfigurazioa Osatuta!", "title": "🎉 Konfigurazioa Osatuta!",
"subtitle": "Zorionak! Zure okindegi kudeaketa sistema erabiltzeko prest dago. Has gaitezen zure lehen zereginekin.", "subtitle": "Zorionak! Zure okindegi kudeaketa sistema erabiltzeko prest dago. Has gaitezen zure lehen zereginekin.",
"congratulations": "Zorionak! Zure Sistema Prest Dago",
"all_configured": "Arrakastaz konfiguratu duzu {name} gure kudeaketa sistema adimendunarekin. Dena prest dago zure okindegi optimizatzen hasteko.",
"what_configured": "Zer Konfiguratu Duzu",
"bakery_info": "Okindegi Informazioa",
"inventory_ai": "IA Inbentarioa",
"suppliers_added": "Hornitzaileak Gehituta",
"recipes_configured": "Errezetak Konfiguratuta",
"quality_set": "Kalitate Estandarrak",
"team_invited": "Taldekideak",
"quick": {
"analytics": "Analitikak",
"inventory": "Inbentarioa",
"procurement": "Erosketak",
"production": "Ekoizpena"
},
"tips_title": "Zure Arrakasta Maximizatzeko Aholkuak",
"tip1": "Berrikusi aginte-panela egunero informazioa lortzeko",
"tip2": "Eguneratu inbentarioa erregularki",
"tip3": "Erabili IA iragarpenak planifikatzeko",
"tip4": "Gonbidatu zure taldea elkarlanean aritzeko",
"go_to_dashboard": "Hasi Sistema Erabiltzen",
"need_help": "Laguntza behar? Bisitatu gure",
"user_guide": "erabiltzaile gida",
"or_contact": "edo kontaktatu gure",
"support_team": "laguntza taldea",
"next_steps": "Gomendatutako Hurrengo Urratsak", "next_steps": "Gomendatutako Hurrengo Urratsak",
"step1_title": "Ekoizpena Hasi", "step1_title": "Ekoizpena Hasi",
"step1_desc": "Sortu zure lehen ekoizpen lotea konfiguratutako errezetek erabiliz", "step1_desc": "Sortu zure lehen ekoizpen lotea konfiguratutako errezetek erabiliz",
@@ -291,7 +333,6 @@
"tip3_desc": "Egiaztatu zure ekoizpen analitikak astero errezetak optimizatzeko eta hondakinak murrizteko", "tip3_desc": "Egiaztatu zure ekoizpen analitikak astero errezetak optimizatzeko eta hondakinak murrizteko",
"tip4_title": "Mantendu Hornitzaileekin Harremanak", "tip4_title": "Mantendu Hornitzaileekin Harremanak",
"tip4_desc": "Mantendu hornitzaileen informazioa eguneratuta eta jarraitu eskaeren errendimendua elkarlantza hobeak lortzeko", "tip4_desc": "Mantendu hornitzaileen informazioa eguneratuta eta jarraitu eskaeren errendimendua elkarlantza hobeak lortzeko",
"need_help": "Laguntza Behar?",
"settings": "Ezarpenak", "settings": "Ezarpenak",
"settings_desc": "Konfiguratu hobespenak", "settings_desc": "Konfiguratu hobespenak",
"dashboard": "Aginte-panela", "dashboard": "Aginte-panela",

View File

@@ -38,6 +38,36 @@ async def get_tenant_members(request: Request, tenant_id: str = Path(...)):
"""Get tenant members""" """Get tenant members"""
return await _proxy_to_tenant_service(request, f"/api/v1/tenants/{tenant_id}/members") return await _proxy_to_tenant_service(request, f"/api/v1/tenants/{tenant_id}/members")
@router.post("/{tenant_id}/members")
async def add_tenant_member(request: Request, tenant_id: str = Path(...)):
"""Add a team member to tenant"""
return await _proxy_to_tenant_service(request, f"/api/v1/tenants/{tenant_id}/members")
@router.post("/{tenant_id}/members/with-user")
async def add_tenant_member_with_user(request: Request, tenant_id: str = Path(...)):
"""Add a team member to tenant with user creation"""
return await _proxy_to_tenant_service(request, f"/api/v1/tenants/{tenant_id}/members/with-user")
@router.put("/{tenant_id}/members/{member_user_id}/role")
async def update_member_role(request: Request, tenant_id: str = Path(...), member_user_id: str = Path(...)):
"""Update team member role"""
return await _proxy_to_tenant_service(request, f"/api/v1/tenants/{tenant_id}/members/{member_user_id}/role")
@router.delete("/{tenant_id}/members/{member_user_id}")
async def remove_tenant_member(request: Request, tenant_id: str = Path(...), member_user_id: str = Path(...)):
"""Remove team member from tenant"""
return await _proxy_to_tenant_service(request, f"/api/v1/tenants/{tenant_id}/members/{member_user_id}")
@router.post("/{tenant_id}/transfer-ownership")
async def transfer_tenant_ownership(request: Request, tenant_id: str = Path(...)):
"""Transfer tenant ownership to another admin"""
return await _proxy_to_tenant_service(request, f"/api/v1/tenants/{tenant_id}/transfer-ownership")
@router.get("/{tenant_id}/admins")
async def get_tenant_admins(request: Request, tenant_id: str = Path(...)):
"""Get all admins for a tenant"""
return await _proxy_to_tenant_service(request, f"/api/v1/tenants/{tenant_id}/admins")
@router.get("/{tenant_id}/hierarchy") @router.get("/{tenant_id}/hierarchy")
async def get_tenant_hierarchy(request: Request, tenant_id: str = Path(...)): async def get_tenant_hierarchy(request: Request, tenant_id: str = Path(...)):
"""Get tenant hierarchy information""" """Get tenant hierarchy information"""