Fix onboarding UI
This commit is contained in:
@@ -39,249 +39,161 @@ export const CompletionStep: React.FC<CompletionStepProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center space-y-10 max-w-5xl mx-auto px-4 animate-fade-in">
|
||||
{/* Confetti effect */}
|
||||
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
||||
<div className="absolute top-0 left-1/4 text-4xl animate-bounce-subtle" style={{ animationDelay: '0s' }}>🎉</div>
|
||||
<div className="absolute top-10 right-1/4 text-3xl animate-bounce-subtle" style={{ animationDelay: '0.5s' }}>✨</div>
|
||||
<div className="absolute top-5 left-1/2 text-4xl animate-bounce-subtle" style={{ animationDelay: '1s' }}>🎊</div>
|
||||
</div>
|
||||
|
||||
{/* Animated Success Icon */}
|
||||
<div className="relative mx-auto w-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 className="text-center space-y-8 max-w-4xl mx-auto px-4 animate-fade-in">
|
||||
{/* Success Icon */}
|
||||
<div className="relative mx-auto w-32 h-32">
|
||||
<div className="absolute inset-0 bg-[var(--color-success)]/20 rounded-full animate-pulse"></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">
|
||||
<CheckCircle2 className="w-16 h-16 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Success Message */}
|
||||
<div className="space-y-5 animate-slide-up">
|
||||
<h1 className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-[var(--color-primary)] via-[var(--color-success)] to-[var(--color-primary)] bg-clip-text text-transparent" style={{ backgroundSize: '200% auto' }}>
|
||||
{t('onboarding:completion.congratulations', '¡Felicidades! Tu Sistema Está Listo')}
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-[var(--text-primary)]">
|
||||
{t('onboarding:completion.congratulations', 'Congratulations! Your System Is Ready')}
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl text-[var(--text-secondary)] max-w-3xl mx-auto leading-relaxed">
|
||||
{t('onboarding:completion.all_configured', 'Has configurado exitosamente {{name}} con nuestro sistema de gestión inteligente. Todo está listo para empezar a optimizar tu panadería.', { name: currentTenant?.name })}
|
||||
<p className="text-base md:text-lg text-[var(--text-secondary)] max-w-2xl mx-auto">
|
||||
{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>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<h3 className="font-bold text-xl md:text-2xl mb-6 text-center text-[var(--text-primary)] flex items-center justify-center gap-3">
|
||||
<span className="text-2xl">📋</span>
|
||||
{t('onboarding:completion.what_configured', 'Lo Que Has Configurado')}
|
||||
<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-lg md:text-xl mb-5 text-center text-[var(--text-primary)]">
|
||||
{t('onboarding:completion.what_configured', 'What You Have Configured')}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-6 h-6 bg-[var(--color-success)]/10 rounded flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<svg className="w-4 h-4 text-[var(--color-success)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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.bakery_info', 'Información de Panadería')}</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">{t('onboarding:completion.bakery_info_desc', 'Datos básicos registrados')}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<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.bakery_info', 'Bakery Information')}</span>
|
||||
</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.inventory_ai', 'Inventario con IA')}</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">{t('onboarding:completion.inventory_ai_desc', 'Productos analizados y categorizados')}</p>
|
||||
</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.inventory_ai', 'AI Inventory')}</span>
|
||||
</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 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 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 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.recipes_configured', 'Recipes Configured')}</span>
|
||||
</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 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.quality_set', 'Quality Standards')}</span>
|
||||
</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 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>
|
||||
|
||||
{/* 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
|
||||
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" />
|
||||
<h4 className="font-bold text-base md:text-lg text-[var(--text-primary)] mb-1.5">
|
||||
{t('onboarding:completion.quick.analytics', 'Analíticas')}
|
||||
<BarChart className="w-8 h-8 text-[var(--color-primary)] mb-2 group-hover:scale-110 transition-transform" />
|
||||
<h4 className="font-semibold text-sm text-[var(--text-primary)]">
|
||||
{t('onboarding:completion.quick.analytics', 'Analytics')}
|
||||
</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
|
||||
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" />
|
||||
<h4 className="font-bold text-base md:text-lg text-[var(--text-primary)] mb-1.5">
|
||||
{t('onboarding:completion.quick.inventory', 'Inventario')}
|
||||
<ShoppingCart className="w-8 h-8 text-[var(--color-success)] mb-2 group-hover:scale-110 transition-transform" />
|
||||
<h4 className="font-semibold text-sm text-[var(--text-primary)]">
|
||||
{t('onboarding:completion.quick.inventory', 'Inventory')}
|
||||
</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
|
||||
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" />
|
||||
<h4 className="font-bold text-base md:text-lg text-[var(--text-primary)] mb-1.5">
|
||||
{t('onboarding:completion.quick.procurement', 'Compras')}
|
||||
<Users className="w-8 h-8 text-[var(--color-info)] mb-2 group-hover:scale-110 transition-transform" />
|
||||
<h4 className="font-semibold text-sm text-[var(--text-primary)]">
|
||||
{t('onboarding:completion.quick.procurement', 'Purchases')}
|
||||
</h4>
|
||||
<p className="text-xs md:text-sm text-[var(--text-secondary)]">
|
||||
{t('onboarding:completion.quick.procurement_desc', 'Gestionar pedidos')}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/app/production')}
|
||||
className="p-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" />
|
||||
<h4 className="font-bold text-base md:text-lg text-[var(--text-primary)] mb-1.5">
|
||||
{t('onboarding:completion.quick.production', 'Producción')}
|
||||
<TrendingUp className="w-8 h-8 text-[var(--color-warning)] mb-2 group-hover:scale-110 transition-transform" />
|
||||
<h4 className="font-semibold text-sm text-[var(--text-primary)]">
|
||||
{t('onboarding:completion.quick.production', 'Production')}
|
||||
</h4>
|
||||
<p className="text-xs md:text-sm text-[var(--text-secondary)]">
|
||||
{t('onboarding:completion.quick.production_desc', 'Planificar producción')}
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tips for Success */}
|
||||
<div className="bg-gradient-to-r from-[var(--color-primary)]/10 via-[var(--color-success)]/10 to-[var(--color-info)]/10 border-2 border-[var(--color-primary)]/30 rounded-2xl p-7 md:p-8 max-w-3xl mx-auto text-left shadow-lg">
|
||||
<div className="flex items-start gap-5">
|
||||
<div className="w-14 h-14 md:w-16 md:h-16 bg-gradient-to-br from-[var(--color-primary)] via-[var(--color-success)] to-[var(--color-info)] text-white rounded-2xl flex items-center justify-center flex-shrink-0 shadow-lg animate-pulse-slow">
|
||||
<Zap className="w-7 h-7 md:w-8 md:h-8" />
|
||||
<div 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-4">
|
||||
<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-6 h-6" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-bold text-xl md:text-2xl mb-4 text-[var(--text-primary)]">
|
||||
{t('onboarding:completion.tips_title', 'Consejos para Maximizar tu Éxito')}
|
||||
<h3 className="font-bold text-lg mb-3 text-[var(--text-primary)]">
|
||||
{t('onboarding:completion.tips_title', 'Tips to Maximize Your Success')}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
<ul className="space-y-2 text-sm text-[var(--text-secondary)]">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]">
|
||||
{t('onboarding:completion.tip1', 'Revisa el dashboard diariamente para insights')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span>{t('onboarding:completion.tip1', 'Review the dashboard daily for insights')}</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]">
|
||||
{t('onboarding:completion.tip2', 'Actualiza el inventario regularmente')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span>{t('onboarding:completion.tip2', 'Update inventory regularly')}</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]">
|
||||
{t('onboarding:completion.tip3', 'Usa las predicciones de IA para planificar')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span>{t('onboarding:completion.tip3', 'Use AI predictions for planning')}</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]">
|
||||
{t('onboarding:completion.tip4', 'Invita a tu equipo para colaborar')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span>{t('onboarding:completion.tip4', 'Invite your team to collaborate')}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Primary Action Button */}
|
||||
<div className="flex justify-center items-center pt-6">
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
onClick={handleExploreDashboard}
|
||||
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>
|
||||
</div>
|
||||
|
||||
{/* Help Text */}
|
||||
<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
|
||||
href="/help"
|
||||
className="text-[var(--color-primary)] hover:underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('onboarding:completion.user_guide', 'guía de usuario')}
|
||||
{t('onboarding:completion.user_guide', 'user guide')}
|
||||
</a>{' '}
|
||||
{t('onboarding:completion.or_contact', 'o contacta a nuestro')}{' '}
|
||||
{t('onboarding:completion.or_contact', 'or contact our')}{' '}
|
||||
<a
|
||||
href="mailto:support@bakery-ia.com"
|
||||
className="text-[var(--color-primary)] hover:underline"
|
||||
>
|
||||
{t('onboarding:completion.support_team', 'equipo de soporte')}
|
||||
{t('onboarding:completion.support_team', 'support team')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,9 @@ export interface RecipeTemplate {
|
||||
totalTime?: number;
|
||||
ingredients: RecipeIngredientTemplate[];
|
||||
instructions?: string;
|
||||
instructionsKey?: string; // Translation key for instructions
|
||||
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'] },
|
||||
],
|
||||
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: [
|
||||
'Do not overmix after adding flour',
|
||||
'Test doneness with toothpick',
|
||||
],
|
||||
tipsKeys: [
|
||||
'recipe_templates:bizcochuelo.tip1',
|
||||
'recipe_templates:bizcochuelo.tip2',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -51,192 +51,71 @@ 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 = () => {
|
||||
onComplete?.({ completed: true });
|
||||
navigate('/app/dashboard');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-4xl mx-auto">
|
||||
{/* Celebration Header */}
|
||||
<div className="text-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-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" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-[var(--text-primary)] mb-3">
|
||||
{t('setup_wizard:completion.title', '🎉 Setup Complete!')}
|
||||
<div className="text-center space-y-8 py-8 max-w-3xl mx-auto">
|
||||
{/* Success Icon */}
|
||||
<div className="mx-auto w-24 h-24 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
|
||||
<svg className="w-12 h-12 text-[var(--color-success)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<p className="text-lg text-[var(--text-secondary)] max-w-2xl mx-auto">
|
||||
{t('setup_wizard:completion.subtitle', "Congratulations! Your bakery management system is ready to use. Let's get started with your first tasks.")}
|
||||
<p className="text-lg text-[var(--text-secondary)]">
|
||||
{t('setup_wizard:completion.subtitle', "Your bakery management system is ready to use.")}
|
||||
</p>
|
||||
</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 */}
|
||||
<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-primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<div className="text-left">
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-4">
|
||||
{t('setup_wizard:completion.next_steps', 'Recommended Next Steps')}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{nextSteps.map((step, index) => (
|
||||
<div
|
||||
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)}
|
||||
>
|
||||
<div className="flex items-start gap-3 mb-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 items-start gap-3">
|
||||
<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}
|
||||
</div>
|
||||
<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}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)] leading-relaxed">
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button className="text-sm font-medium text-[var(--color-primary)] hover:underline flex items-center gap-1">
|
||||
{step.action}
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</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">
|
||||
{/* Action Button */}
|
||||
<div className="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"
|
||||
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"
|
||||
>
|
||||
<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')}
|
||||
<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" />
|
||||
</svg>
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 { 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 }) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -21,9 +21,12 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
|
||||
// Mutations
|
||||
const createTemplateMutation = useCreateQualityTemplate(tenantId);
|
||||
const updateTemplateMutation = useUpdateQualityTemplate(tenantId);
|
||||
const deleteTemplateMutation = useDeleteQualityTemplate(tenantId);
|
||||
|
||||
// Form state
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
check_type: QualityCheckType.VISUAL,
|
||||
@@ -31,6 +34,14 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
applicable_stages: [] as ProcessStage[],
|
||||
is_required: 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>>({});
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
// 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);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
@@ -72,7 +113,7 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
if (!validateForm()) return;
|
||||
|
||||
try {
|
||||
const templateData: QualityCheckTemplateCreate = {
|
||||
const templateData: any = {
|
||||
name: formData.name,
|
||||
check_type: (formData.check_type as QualityCheckType) || QualityCheckType.VISUAL,
|
||||
description: formData.description || undefined,
|
||||
@@ -81,10 +122,37 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
is_critical: formData.is_critical,
|
||||
is_active: true,
|
||||
weight: formData.is_critical ? 10 : 5,
|
||||
created_by: userId || '',
|
||||
};
|
||||
|
||||
await createTemplateMutation.mutateAsync(templateData);
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Reset form
|
||||
resetForm();
|
||||
@@ -101,16 +169,47 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
applicable_stages: [],
|
||||
is_required: false,
|
||||
is_critical: false,
|
||||
unit: '',
|
||||
min_value: '',
|
||||
max_value: '',
|
||||
target_value: '',
|
||||
tolerance_percentage: '',
|
||||
scoring_criteria: {},
|
||||
});
|
||||
setErrors({});
|
||||
setIsAdding(false);
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
const toggleStage = (stage: ProcessStage) => {
|
||||
const stages = formData.applicable_stages.includes(stage)
|
||||
? formData.applicable_stages.filter((s) => s !== stage)
|
||||
: [...formData.applicable_stages, stage];
|
||||
setFormData({ ...formData, applicable_stages: stages });
|
||||
const handleEdit = (template: QualityCheckTemplate) => {
|
||||
setFormData({
|
||||
name: template.name,
|
||||
check_type: template.check_type,
|
||||
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 = [
|
||||
@@ -218,6 +317,29 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
)}
|
||||
</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>
|
||||
@@ -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)]">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<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>
|
||||
<button
|
||||
type="button"
|
||||
@@ -251,7 +376,7 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className={`w - full px - 3 py - 2 bg - [var(--bg - primary)] border ${errors.name ? 'border-[var(--color-error)]' : 'border-[var(--border-secondary)]'} rounded - lg focus: outline - none focus: ring - 2 focus: ring - [var(--color - primary)]text - [var(--text - primary)]`}
|
||||
className={`w-full px-3 py-2 bg-[var(--bg-primary)] border ${errors.name ? 'border-[var(--color-error)]' : 'border-[var(--border-secondary)]'} rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]`}
|
||||
placeholder={t('setup_wizard:quality.placeholders.name', 'e.g., Crust color check, Dough temperature')}
|
||||
/>
|
||||
{errors.name && <p className="mt-1 text-xs text-[var(--color-error)]">{errors.name}</p>}
|
||||
@@ -300,6 +425,115 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
/>
|
||||
</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 */}
|
||||
<div>
|
||||
<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">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
{createTemplateMutation.isPending ? (
|
||||
{(createTemplateMutation.isPending || updateTemplateMutation.isPending) ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<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" />
|
||||
@@ -381,6 +615,8 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
</svg>
|
||||
{t('common:saving', 'Saving...')}
|
||||
</span>
|
||||
) : editingId ? (
|
||||
t('common:update', 'Update')
|
||||
) : (
|
||||
t('common:add', 'Add')
|
||||
)}
|
||||
|
||||
@@ -38,6 +38,7 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
|
||||
// Form state
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
@@ -108,23 +109,48 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
if (!validateForm()) return;
|
||||
|
||||
try {
|
||||
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)),
|
||||
};
|
||||
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 createRecipeMutation.mutateAsync(recipeData);
|
||||
await updateRecipeMutation.mutateAsync({
|
||||
id: editingId,
|
||||
data: recipeData,
|
||||
});
|
||||
} else {
|
||||
// Create new 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 createRecipeMutation.mutateAsync(recipeData);
|
||||
}
|
||||
|
||||
// Reset form
|
||||
resetForm();
|
||||
@@ -145,6 +171,43 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
setRecipeIngredients([]);
|
||||
setErrors({});
|
||||
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) => {
|
||||
@@ -246,15 +309,15 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
};
|
||||
|
||||
const unitOptions = [
|
||||
{ value: MeasurementUnit.GRAMS, label: t('recipes:unit.g', 'Grams (g)') },
|
||||
{ value: MeasurementUnit.KILOGRAMS, label: t('recipes:unit.kg', 'Kilograms (kg)') },
|
||||
{ value: MeasurementUnit.MILLILITERS, label: t('recipes:unit.ml', 'Milliliters (ml)') },
|
||||
{ value: MeasurementUnit.LITERS, label: t('recipes:unit.l', 'Liters (l)') },
|
||||
{ value: MeasurementUnit.UNITS, label: t('recipes:unit.units', 'Units') },
|
||||
{ value: MeasurementUnit.PIECES, label: t('recipes:unit.pieces', 'Pieces') },
|
||||
{ value: MeasurementUnit.CUPS, label: t('recipes:unit.cups', 'Cups') },
|
||||
{ value: MeasurementUnit.TABLESPOONS, label: t('recipes:unit.tbsp', 'Tablespoons') },
|
||||
{ value: MeasurementUnit.TEASPOONS, label: t('recipes:unit.tsp', 'Teaspoons') },
|
||||
{ value: MeasurementUnit.GRAMS, label: t('recipes:units.g', 'Grams (g)') },
|
||||
{ value: MeasurementUnit.KILOGRAMS, label: t('recipes:units.kg', 'Kilograms (kg)') },
|
||||
{ value: MeasurementUnit.MILLILITERS, label: t('recipes:units.ml', 'Milliliters (ml)') },
|
||||
{ value: MeasurementUnit.LITERS, label: t('recipes:units.l', 'Liters (l)') },
|
||||
{ value: MeasurementUnit.UNITS, label: t('recipes:units.units', 'Units') },
|
||||
{ value: MeasurementUnit.PIECES, label: t('recipes:units.pieces', 'Pieces') },
|
||||
{ value: MeasurementUnit.CUPS, label: t('recipes:units.cups', 'Cups') },
|
||||
{ value: MeasurementUnit.TABLESPOONS, label: t('recipes:units.tbsp', 'Tablespoons') },
|
||||
{ value: MeasurementUnit.TEASPOONS, label: t('recipes:units.tsp', 'Teaspoons') },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -354,19 +417,26 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{template.instructions && (
|
||||
{(template.instructions || template.instructionsKey) && (
|
||||
<div>
|
||||
<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>
|
||||
)}
|
||||
{template.tips && template.tips.length > 0 && (
|
||||
{((template.tips && template.tips.length > 0) || (template.tipsKeys && template.tipsKeys.length > 0)) && (
|
||||
<div>
|
||||
<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)]">
|
||||
{template.tips.map((tip, idx) => (
|
||||
<li key={idx}>{tip}</li>
|
||||
))}
|
||||
{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>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</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" />
|
||||
</svg>
|
||||
<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>
|
||||
</div>
|
||||
{recipes.length >= 1 && (
|
||||
@@ -495,6 +567,16 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
type="button"
|
||||
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)]">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<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>
|
||||
<button
|
||||
type="button"
|
||||
@@ -679,7 +761,7 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
>
|
||||
{unitOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.value}
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -708,10 +790,10 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
<div className="flex gap-2 pt-2">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
{createRecipeMutation.isPending ? (
|
||||
{(createRecipeMutation.isPending || updateRecipeMutation.isPending) ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<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" />
|
||||
@@ -719,6 +801,8 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
</svg>
|
||||
{t('common:saving', 'Saving...')}
|
||||
</span>
|
||||
) : editingId ? (
|
||||
t('common:update', 'Update')
|
||||
) : (
|
||||
t('common:add', 'Add')
|
||||
)}
|
||||
|
||||
@@ -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" />
|
||||
</svg>
|
||||
<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>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -69,7 +69,9 @@
|
||||
"expand": "Expand",
|
||||
"collapse": "Collapse",
|
||||
"save_draft": "Save Draft",
|
||||
"confirm_receipt": "Confirm Receipt"
|
||||
"confirm_receipt": "Confirm Receipt",
|
||||
"hide": "Hide",
|
||||
"preview": "Preview"
|
||||
},
|
||||
"saved": "saved",
|
||||
"item": "item",
|
||||
|
||||
7
frontend/src/locales/en/recipe_templates.json
Normal file
7
frontend/src/locales/en/recipe_templates.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
"added_count": "{count} recipe 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",
|
||||
"yield_label": "Yield",
|
||||
"add_recipe": "Add Recipe",
|
||||
@@ -189,23 +189,40 @@
|
||||
"recommended": "2+ recommended (optional)",
|
||||
"your_checks": "Your Quality Checks",
|
||||
"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_another": "Add Another Quality Check",
|
||||
"measurement_settings": "Measurement Settings",
|
||||
"measurement_help": "Define the acceptable range and target for this measurement.",
|
||||
"fields": {
|
||||
"name": "Check Name",
|
||||
"check_type": "Check Type",
|
||||
"description": "Description",
|
||||
"stages": "Applicable Stages",
|
||||
"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": {
|
||||
"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": {
|
||||
"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": {
|
||||
@@ -272,6 +289,31 @@
|
||||
"completion": {
|
||||
"title": "🎉 Setup Complete!",
|
||||
"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",
|
||||
"step1_title": "Start Production",
|
||||
"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",
|
||||
"tip4_title": "Maintain Supplier Relationships",
|
||||
"tip4_desc": "Keep supplier information current and track order performance for better partnerships",
|
||||
"need_help": "Need Help?",
|
||||
"settings": "Settings",
|
||||
"settings_desc": "Configure preferences",
|
||||
"dashboard": "Dashboard",
|
||||
|
||||
@@ -69,7 +69,9 @@
|
||||
"expand": "Expandir",
|
||||
"collapse": "Contraer",
|
||||
"save_draft": "Guardar Borrador",
|
||||
"confirm_receipt": "Confirmar Recepción"
|
||||
"confirm_receipt": "Confirmar Recepción",
|
||||
"hide": "Ocultar",
|
||||
"preview": "Vista previa"
|
||||
},
|
||||
"saved": "ahorrado",
|
||||
"item": "artículo",
|
||||
|
||||
7
frontend/src/locales/es/recipe_templates.json
Normal file
7
frontend/src/locales/es/recipe_templates.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
"added_count": "{count} receta agregada",
|
||||
"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",
|
||||
"yield_label": "Rendimiento",
|
||||
"add_recipe": "Agregar Receta",
|
||||
@@ -189,23 +189,40 @@
|
||||
"recommended": "2+ recomendados (opcional)",
|
||||
"your_checks": "Tus Controles 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_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": {
|
||||
"name": "Nombre del Control",
|
||||
"check_type": "Tipo de Control",
|
||||
"description": "Descripción",
|
||||
"stages": "Etapas Aplicables",
|
||||
"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": {
|
||||
"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": {
|
||||
"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": {
|
||||
@@ -272,6 +289,31 @@
|
||||
"completion": {
|
||||
"title": "🎉 ¡Configuración Completa!",
|
||||
"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",
|
||||
"step1_title": "Iniciar Producción",
|
||||
"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",
|
||||
"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",
|
||||
"need_help": "¿Necesitas Ayuda?",
|
||||
"settings": "Configuración",
|
||||
"settings_desc": "Configurar preferencias",
|
||||
"dashboard": "Panel",
|
||||
|
||||
@@ -67,7 +67,9 @@
|
||||
"expand": "Zabaldu",
|
||||
"collapse": "Tolestu",
|
||||
"save_draft": "Zirriborroa Gorde",
|
||||
"confirm_receipt": "Harrera Berretsi"
|
||||
"confirm_receipt": "Harrera Berretsi",
|
||||
"hide": "Ezkutatu",
|
||||
"preview": "Aurrebista"
|
||||
},
|
||||
"saved": "aurreztuta",
|
||||
"item": "produktua",
|
||||
|
||||
7
frontend/src/locales/eu/recipe_templates.json
Normal file
7
frontend/src/locales/eu/recipe_templates.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -147,7 +147,7 @@
|
||||
"prerequisites_desc": "Gutxienez 2 osagai behar dituzu zure inbentarioan errezetak sortu aurretik. Itzuli Inbentario urratsera osagai gehiago gehitzeko.",
|
||||
"added_count": "Errezeta {count} gehituta",
|
||||
"added_count_plural": "{count} errezeta gehituta",
|
||||
"minimum_met": "{count} errezeta gehituta - Jarraitzeko prest!",
|
||||
"minimum_met": "Jarraitzeko prest!",
|
||||
"your_recipes": "Zure Errezetak",
|
||||
"yield_label": "Etekin",
|
||||
"add_recipe": "Errezeta Gehitu",
|
||||
@@ -189,23 +189,40 @@
|
||||
"recommended": "2+ gomendatzen dira (aukerakoa)",
|
||||
"your_checks": "Zure Kalitate Kontrolak",
|
||||
"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_another": "Beste Kalitate Kontrol Bat Gehitu",
|
||||
"measurement_settings": "Neurketa Ezarpenak",
|
||||
"measurement_help": "Definitu neurketa honetarako tartea eta helburu onargarria.",
|
||||
"fields": {
|
||||
"name": "Kontrolaren Izena",
|
||||
"check_type": "Kontrol Mota",
|
||||
"description": "Deskribapena",
|
||||
"stages": "Etapa Aplikagarriak",
|
||||
"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": {
|
||||
"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": {
|
||||
"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": {
|
||||
@@ -272,6 +289,31 @@
|
||||
"completion": {
|
||||
"title": "🎉 Konfigurazioa Osatuta!",
|
||||
"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",
|
||||
"step1_title": "Ekoizpena Hasi",
|
||||
"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",
|
||||
"tip4_title": "Mantendu Hornitzaileekin Harremanak",
|
||||
"tip4_desc": "Mantendu hornitzaileen informazioa eguneratuta eta jarraitu eskaeren errendimendua elkarlantza hobeak lortzeko",
|
||||
"need_help": "Laguntza Behar?",
|
||||
"settings": "Ezarpenak",
|
||||
"settings_desc": "Konfiguratu hobespenak",
|
||||
"dashboard": "Aginte-panela",
|
||||
|
||||
@@ -38,6 +38,36 @@ async def get_tenant_members(request: Request, tenant_id: str = Path(...)):
|
||||
"""Get tenant 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")
|
||||
async def get_tenant_hierarchy(request: Request, tenant_id: str = Path(...)):
|
||||
"""Get tenant hierarchy information"""
|
||||
|
||||
Reference in New Issue
Block a user