Merge pull request #16 from ualsweb/claude/improve-onboarding-wizard-011CV4ANGNngveTMSN3J29xZ
Claude/improve onboarding wizard 011 cv4 ang nngve tmsn3 j29x z
This commit is contained in:
@@ -11,7 +11,6 @@ import { WizardProvider, useWizardContext, BakeryType, DataSource } from './cont
|
||||
import {
|
||||
BakeryTypeSelectionStep,
|
||||
RegisterTenantStep,
|
||||
POIDetectionStep,
|
||||
FileUploadStep,
|
||||
InventoryReviewStep,
|
||||
ProductCategorizationStep,
|
||||
@@ -75,15 +74,7 @@ const OnboardingWizardContent: React.FC = () => {
|
||||
isConditional: true,
|
||||
condition: (ctx) => ctx.state.bakeryType !== null,
|
||||
},
|
||||
// Phase 2b: POI Detection
|
||||
{
|
||||
id: 'poi-detection',
|
||||
title: t('onboarding:steps.poi_detection.title', 'Detección de Ubicación'),
|
||||
description: t('onboarding:steps.poi_detection.description', 'Analizar puntos de interés cercanos'),
|
||||
component: POIDetectionStep,
|
||||
isConditional: true,
|
||||
condition: (ctx) => ctx.state.bakeryType !== null && ctx.state.bakeryLocation !== undefined,
|
||||
},
|
||||
// POI Detection removed - now happens automatically in background after tenant registration
|
||||
// Phase 2a: AI-Assisted Inventory Setup (REFACTORED - split into 3 focused steps)
|
||||
{
|
||||
id: 'upload-sales-data',
|
||||
@@ -159,14 +150,7 @@ const OnboardingWizardContent: React.FC = () => {
|
||||
component: MLTrainingStep,
|
||||
// Always show - no conditional
|
||||
},
|
||||
{
|
||||
id: 'setup-review',
|
||||
title: t('onboarding:steps.review.title', 'Revisión'),
|
||||
description: t('onboarding:steps.review.description', 'Confirma tu configuración'),
|
||||
component: ReviewSetupStep,
|
||||
isConditional: true,
|
||||
condition: (ctx) => ctx.state.bakeryType !== null, // Tenant created after bakeryType is set
|
||||
},
|
||||
// Revision step removed - not useful for user, completion step is final step
|
||||
{
|
||||
id: 'completion',
|
||||
title: t('onboarding:steps.completion.title', 'Completado'),
|
||||
@@ -425,6 +409,16 @@ const OnboardingWizardContent: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoToPrevious = () => {
|
||||
if (currentStepIndex > 0) {
|
||||
const previousStep = VISIBLE_STEPS[currentStepIndex - 1];
|
||||
console.log(`⬅️ Going back from "${currentStep.id}" to "${previousStep.id}"`);
|
||||
setCurrentStepIndex(currentStepIndex - 1);
|
||||
} else {
|
||||
console.warn('⚠️ Already at first step, cannot go back');
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading state
|
||||
if (!isNewTenant && (isLoadingProgress || !isInitialized)) {
|
||||
return (
|
||||
@@ -534,7 +528,7 @@ const OnboardingWizardContent: React.FC = () => {
|
||||
<CardBody padding="md">
|
||||
<StepComponent
|
||||
onNext={() => {}}
|
||||
onPrevious={() => {}}
|
||||
onPrevious={handleGoToPrevious}
|
||||
onComplete={handleStepComplete}
|
||||
onUpdate={handleStepUpdate}
|
||||
isFirstStep={currentStepIndex === 0}
|
||||
@@ -562,12 +556,6 @@ const OnboardingWizardContent: React.FC = () => {
|
||||
initialStock: undefined,
|
||||
}))
|
||||
}
|
||||
: // Pass tenant info to POI detection step
|
||||
currentStep.id === 'poi-detection'
|
||||
? {
|
||||
tenantId: wizardContext.state.tenantId,
|
||||
bakeryLocation: wizardContext.state.bakeryLocation,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -269,7 +269,7 @@ export const WizardProvider: React.FC<WizardProviderProps> = ({
|
||||
steps.push('ml-training');
|
||||
}
|
||||
|
||||
steps.push('setup-review');
|
||||
// Revision step removed - not useful for user
|
||||
steps.push('completion');
|
||||
|
||||
return steps;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '../../../ui/Button';
|
||||
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||||
import { ChartBar, ShoppingCart, Users, TrendingUp, Zap, CheckCircle2 } from 'lucide-react';
|
||||
|
||||
interface CompletionStepProps {
|
||||
onNext: () => void;
|
||||
@@ -30,20 +31,21 @@ export const CompletionStep: React.FC<CompletionStepProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center space-y-8">
|
||||
{/* Success Icon */}
|
||||
<div className="mx-auto w-24 h-24 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
|
||||
<svg className="w-12 h-12 text-[var(--color-success)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<div className="text-center space-y-8 max-w-5xl mx-auto">
|
||||
{/* Animated Success Icon */}
|
||||
<div className="relative mx-auto w-32 h-32">
|
||||
<div className="absolute inset-0 bg-[var(--color-success)]/20 rounded-full animate-ping"></div>
|
||||
<div className="relative w-32 h-32 bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success)]/70 rounded-full flex items-center justify-center shadow-lg">
|
||||
<CheckCircle2 className="w-16 h-16 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Success Message */}
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-3xl font-bold text-[var(--text-primary)]">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-success)] bg-clip-text text-transparent">
|
||||
{t('onboarding:completion.congratulations', '¡Felicidades! Tu Sistema Está Listo')}
|
||||
</h1>
|
||||
<p className="text-lg text-[var(--text-secondary)] max-w-2xl mx-auto">
|
||||
<p className="text-xl text-[var(--text-secondary)] max-w-2xl mx-auto">
|
||||
{t('onboarding:completion.all_configured', 'Has configurado exitosamente {{name}} con nuestro sistema de gestión inteligente. Todo está listo para empezar a optimizar tu panadería.', { name: currentTenant?.name })}
|
||||
</p>
|
||||
</div>
|
||||
@@ -140,49 +142,109 @@ export const CompletionStep: React.FC<CompletionStepProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Next Steps */}
|
||||
<div className="bg-gradient-to-r from-[var(--color-primary)]/10 to-[var(--color-primary)]/5 border border-[var(--color-primary)]/20 rounded-lg p-6 max-w-2xl mx-auto text-left">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 bg-[var(--color-primary)] text-white rounded-full flex items-center justify-center text-xl font-bold flex-shrink-0">
|
||||
🚀
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2 text-[var(--text-primary)]">{t('onboarding:completion.ready_to_start', '¡Listo para Empezar!')}</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-3">
|
||||
{t('onboarding:completion.explore_message', 'Ahora puedes explorar el panel de control y comenzar a gestionar tu panadería con inteligencia artificial.')}
|
||||
{/* Quick Access Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 max-w-4xl mx-auto">
|
||||
<button
|
||||
onClick={() => navigate('/app/dashboard')}
|
||||
className="p-4 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] rounded-lg transition-all hover:shadow-lg hover:scale-105 text-left group"
|
||||
>
|
||||
<ChartBar className="w-8 h-8 text-[var(--color-primary)] mb-2 group-hover:scale-110 transition-transform" />
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-1">
|
||||
{t('onboarding:completion.quick.analytics', 'Analíticas')}
|
||||
</h4>
|
||||
<p className="text-xs text-[var(--text-secondary)]">
|
||||
{t('onboarding:completion.quick.analytics_desc', 'Ver predicciones y métricas')}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<svg className="w-4 h-4 text-[var(--color-primary)] mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
<span className="text-[var(--text-secondary)]">{t('onboarding:completion.view_analytics', 'Ve análisis y predicciones de demanda')}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/app/inventory')}
|
||||
className="p-4 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] rounded-lg transition-all hover:shadow-lg hover:scale-105 text-left group"
|
||||
>
|
||||
<ShoppingCart className="w-8 h-8 text-[var(--color-success)] mb-2 group-hover:scale-110 transition-transform" />
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-1">
|
||||
{t('onboarding:completion.quick.inventory', 'Inventario')}
|
||||
</h4>
|
||||
<p className="text-xs text-[var(--text-secondary)]">
|
||||
{t('onboarding:completion.quick.inventory_desc', 'Gestionar stock y productos')}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/app/procurement')}
|
||||
className="p-4 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] rounded-lg transition-all hover:shadow-lg hover:scale-105 text-left group"
|
||||
>
|
||||
<Users className="w-8 h-8 text-[var(--color-info)] mb-2 group-hover:scale-110 transition-transform" />
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-1">
|
||||
{t('onboarding:completion.quick.procurement', 'Compras')}
|
||||
</h4>
|
||||
<p className="text-xs text-[var(--text-secondary)]">
|
||||
{t('onboarding:completion.quick.procurement_desc', 'Gestionar pedidos')}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/app/production')}
|
||||
className="p-4 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] rounded-lg transition-all hover:shadow-lg hover:scale-105 text-left group"
|
||||
>
|
||||
<TrendingUp className="w-8 h-8 text-[var(--color-warning)] mb-2 group-hover:scale-110 transition-transform" />
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-1">
|
||||
{t('onboarding:completion.quick.production', 'Producción')}
|
||||
</h4>
|
||||
<p className="text-xs text-[var(--text-secondary)]">
|
||||
{t('onboarding:completion.quick.production_desc', 'Planificar producción')}
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<svg className="w-4 h-4 text-[var(--color-primary)] mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span className="text-[var(--text-secondary)]">{t('onboarding:completion.manage_operations', 'Gestiona producción y operaciones diarias')}</span>
|
||||
|
||||
{/* Tips for Success */}
|
||||
<div className="bg-gradient-to-r from-[var(--color-primary)]/10 to-[var(--color-info)]/10 border border-[var(--color-primary)]/20 rounded-xl p-6 max-w-3xl mx-auto text-left">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-info)] text-white rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<Zap className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<svg className="w-4 h-4 text-[var(--color-primary)] mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
|
||||
</svg>
|
||||
<span className="text-[var(--text-secondary)]">{t('onboarding:completion.optimize_costs', 'Optimiza costos y reduce desperdicios')}</span>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-bold text-lg mb-3 text-[var(--text-primary)]">
|
||||
{t('onboarding:completion.tips_title', 'Consejos para Maximizar tu Éxito')}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]">
|
||||
{t('onboarding:completion.tip1', 'Revisa el dashboard diariamente para insights')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]">
|
||||
{t('onboarding:completion.tip2', 'Actualiza el inventario regularmente')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]">
|
||||
{t('onboarding:completion.tip3', 'Usa las predicciones de IA para planificar')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" />
|
||||
<span className="text-[var(--text-secondary)]">
|
||||
{t('onboarding:completion.tip4', 'Invita a tu equipo para colaborar')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
{/* Primary Action Button */}
|
||||
<div className="flex justify-center items-center pt-4">
|
||||
<Button
|
||||
onClick={handleExploreDashboard}
|
||||
size="lg"
|
||||
className="px-8"
|
||||
className="px-12 py-4 text-lg font-semibold shadow-lg hover:shadow-xl transition-all"
|
||||
>
|
||||
{t('onboarding:completion.go_to_dashboard', 'Ir al Panel de Control →')}
|
||||
{t('onboarding:completion.go_to_dashboard', 'Comenzar a Usar el Sistema →')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
|
||||
const [error, setError] = useState<string>('');
|
||||
const [progressState, setProgressState] = useState<ProgressState | null>(null);
|
||||
const [showGuide, setShowGuide] = useState(false);
|
||||
const [validationSuccess, setValidationSuccess] = useState(false);
|
||||
const [validationDetails, setValidationDetails] = useState<{rows: number, products: number} | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
@@ -101,6 +103,13 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
// Show validation success feedback
|
||||
setValidationSuccess(true);
|
||||
setValidationDetails({
|
||||
rows: validationResult.total_rows || 0,
|
||||
products: validationResult.product_list?.length || 0
|
||||
});
|
||||
|
||||
// Step 2: Extract product list
|
||||
setProgressState({
|
||||
stage: 'analyzing',
|
||||
@@ -158,6 +167,8 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
|
||||
setSelectedFile(null);
|
||||
setError('');
|
||||
setProgressState(null);
|
||||
setValidationSuccess(false);
|
||||
setValidationDetails(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
@@ -215,14 +226,14 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
|
||||
)}
|
||||
|
||||
{/* Selected File Preview */}
|
||||
{selectedFile && !isProcessing && (
|
||||
<div className="border border-[var(--color-success)] bg-[var(--color-success)]/5 rounded-lg p-3 md:p-4">
|
||||
{selectedFile && !isProcessing && !validationSuccess && (
|
||||
<div className="border-2 border-[var(--color-primary)] bg-[var(--color-primary)]/5 rounded-lg p-3 md:p-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 md:gap-3 min-w-0">
|
||||
<FileText className="w-8 h-8 md:w-10 md:h-10 text-[var(--color-success)] flex-shrink-0" />
|
||||
<FileText className="w-8 h-8 md:w-10 md:h-10 text-[var(--color-primary)] flex-shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-[var(--text-primary)] text-sm md:text-base truncate">{selectedFile.name}</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
<p className="text-xs text-[var(--text-secondary)]">
|
||||
{(selectedFile.size / 1024).toFixed(2)} KB
|
||||
</p>
|
||||
</div>
|
||||
@@ -237,6 +248,40 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Validation Success State */}
|
||||
{selectedFile && validationSuccess && !isProcessing && validationDetails && (
|
||||
<div className="border-2 border-[var(--color-success)] bg-[var(--color-success)]/10 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-[var(--color-success)] flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-[var(--color-success)] mb-2">
|
||||
{t('onboarding:file_upload.validation_success', '¡Archivo validado correctamente!')}
|
||||
</p>
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[var(--text-secondary)]">{t('onboarding:file_upload.file_name', 'Archivo:')}</span>
|
||||
<span className="font-medium text-[var(--text-primary)]">{selectedFile.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[var(--text-secondary)]">{t('onboarding:file_upload.rows_found', 'Registros encontrados:')}</span>
|
||||
<span className="font-medium text-[var(--text-primary)]">{validationDetails.rows}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[var(--text-secondary)]">{t('onboarding:file_upload.products_found', 'Productos únicos:')}</span>
|
||||
<span className="font-medium text-[var(--text-primary)]">{validationDetails.products}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRemoveFile}
|
||||
className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)] mt-2 hover:underline"
|
||||
>
|
||||
{t('onboarding:file_upload.change_file', 'Cambiar archivo')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress Indicator */}
|
||||
{isProcessing && progressState && (
|
||||
<div className="border border-[var(--color-primary)] rounded-lg p-6 bg-[var(--color-primary)]/5">
|
||||
@@ -260,12 +305,24 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="bg-[var(--color-danger)]/10 border border-[var(--color-danger)]/20 rounded-lg p-4">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="w-5 h-5 text-[var(--color-danger)] flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-[var(--color-danger)] mb-1">Error</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">{error}</p>
|
||||
<div className="bg-[var(--color-error)]/10 border-2 border-[var(--color-error)] rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-6 h-6 text-[var(--color-error)] flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-[var(--color-error)] mb-2">
|
||||
{t('onboarding:file_upload.validation_failed', 'Error al validar el archivo')}
|
||||
</p>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-3">{error}</p>
|
||||
<div className="bg-[var(--bg-secondary)] rounded p-3 text-xs space-y-1">
|
||||
<p className="font-medium text-[var(--text-primary)] mb-1">
|
||||
{t('onboarding:file_upload.error_tips', 'Consejos para solucionar el problema:')}
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-0.5 text-[var(--text-secondary)]">
|
||||
<li>{t('onboarding:file_upload.tip_1', 'Verifica que el archivo tenga las columnas: Fecha, Producto, Cantidad')}</li>
|
||||
<li>{t('onboarding:file_upload.tip_2', 'Asegúrate de que las fechas estén en formato YYYY-MM-DD')}</li>
|
||||
<li>{t('onboarding:file_upload.tip_3', 'Comprueba que no haya filas vacías o datos incorrectos')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,9 @@ import { Package, Salad, AlertCircle, ArrowRight, ArrowLeft, CheckCircle } from
|
||||
import Button from '../../../ui/Button/Button';
|
||||
import Card from '../../../ui/Card/Card';
|
||||
import Input from '../../../ui/Input/Input';
|
||||
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||||
import { useAddStock } from '../../../../api/hooks/inventory';
|
||||
import InfoCard from '../../../ui/InfoCard';
|
||||
|
||||
export interface ProductWithStock {
|
||||
id: string;
|
||||
@@ -32,6 +35,11 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
|
||||
initialData,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const currentTenant = useCurrentTenant();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
const addStockMutation = useAddStock();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const [products, setProducts] = useState<ProductWithStock[]>(() => {
|
||||
if (initialData?.productsWithStock) {
|
||||
return initialData.productsWithStock;
|
||||
@@ -76,8 +84,36 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
|
||||
onComplete?.();
|
||||
};
|
||||
|
||||
const handleContinue = () => {
|
||||
const handleContinue = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// Create stock entries for products with initial stock > 0
|
||||
const stockEntries = products.filter(p => p.initialStock && p.initialStock > 0);
|
||||
|
||||
if (stockEntries.length > 0) {
|
||||
// Create stock entries in parallel
|
||||
const stockPromises = stockEntries.map(product =>
|
||||
addStockMutation.mutateAsync({
|
||||
tenantId,
|
||||
stockData: {
|
||||
ingredient_id: product.id,
|
||||
unit_price: 0, // Default price, can be updated later
|
||||
notes: `Initial stock entry from onboarding`
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(stockPromises);
|
||||
console.log(`✅ Created ${stockEntries.length} stock entries successfully`);
|
||||
}
|
||||
|
||||
onComplete?.();
|
||||
} catch (error) {
|
||||
console.error('Error creating stock entries:', error);
|
||||
alert(t('onboarding:stock.error_creating_stock', 'Error al crear los niveles de stock. Por favor, inténtalo de nuevo.'));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const productsWithStock = products.filter(p => p.initialStock !== undefined && p.initialStock >= 0);
|
||||
@@ -106,51 +142,30 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-4 md:p-6 space-y-4 md:space-y-6">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-2 md:space-y-3">
|
||||
<h1 className="text-xl md:text-2xl font-bold text-text-primary px-2">
|
||||
{t('onboarding:stock.title', 'Niveles de Stock Inicial')}
|
||||
</h1>
|
||||
<p className="text-sm md:text-base text-text-secondary max-w-2xl mx-auto px-4">
|
||||
{t(
|
||||
'onboarding:stock.subtitle',
|
||||
'Ingresa las cantidades actuales de cada producto. Esto permite que el sistema rastree el inventario desde hoy.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Info Banner */}
|
||||
<Card className="bg-blue-50 border-blue-200">
|
||||
<div className="p-4 flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-blue-900">
|
||||
<p className="font-medium mb-1">
|
||||
{t('onboarding:stock.info_title', '¿Por qué es importante?')}
|
||||
</p>
|
||||
<p className="text-blue-700">
|
||||
{t(
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
{/* Why This Matters */}
|
||||
<InfoCard
|
||||
variant="info"
|
||||
title={t('setup_wizard:why_this_matters', '¿Por qué es importante?')}
|
||||
description={t(
|
||||
'onboarding:stock.info_text',
|
||||
'Sin niveles de stock iniciales, el sistema no puede alertarte sobre stock bajo, planificar producción o calcular costos correctamente. Tómate un momento para ingresar tus cantidades actuales.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
/>
|
||||
|
||||
{/* Progress */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-text-secondary">
|
||||
<span className="text-[var(--text-secondary)]">
|
||||
{t('onboarding:stock.progress', 'Progreso de captura')}
|
||||
</span>
|
||||
<span className="font-medium text-text-primary">
|
||||
<span className="font-medium text-[var(--text-primary)]">
|
||||
{productsWithStock.length} / {products.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-2">
|
||||
<div
|
||||
className="bg-primary-500 h-2 rounded-full transition-all duration-300"
|
||||
className="bg-[var(--color-primary)] h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${completionPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -170,10 +185,10 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
|
||||
{ingredients.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<Salad className="w-4 h-4 text-green-600" />
|
||||
<div className="w-8 h-8 bg-[var(--color-success)]/10 dark:bg-[var(--color-success)]/20 rounded-lg flex items-center justify-center">
|
||||
<Salad className="w-4 h-4 text-[var(--color-success)]" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-text-primary">
|
||||
<h3 className="font-semibold text-[var(--text-primary)]">
|
||||
{t('onboarding:stock.ingredients', 'Ingredientes')} ({ingredients.length})
|
||||
</h3>
|
||||
</div>
|
||||
@@ -182,16 +197,16 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
|
||||
{ingredients.map(product => {
|
||||
const hasStock = product.initialStock !== undefined;
|
||||
return (
|
||||
<Card key={product.id} className={hasStock ? 'bg-green-50 border-green-200' : ''}>
|
||||
<Card key={product.id} className={hasStock ? 'bg-[var(--color-success)]/10 dark:bg-[var(--color-success)]/20 border-[var(--color-success)]/30' : ''}>
|
||||
<div className="p-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-text-primary flex items-center gap-2">
|
||||
<div className="font-medium text-[var(--text-primary)] flex items-center gap-2">
|
||||
{product.name}
|
||||
{hasStock && <CheckCircle className="w-4 h-4 text-green-600" />}
|
||||
{hasStock && <CheckCircle className="w-4 h-4 text-[var(--color-success)]" />}
|
||||
</div>
|
||||
{product.category && (
|
||||
<div className="text-xs text-text-secondary">{product.category}</div>
|
||||
<div className="text-xs text-[var(--text-secondary)]">{product.category}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -204,7 +219,7 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
|
||||
step="0.01"
|
||||
className="w-20 sm:w-24 text-right min-h-[44px]"
|
||||
/>
|
||||
<span className="text-sm text-text-secondary whitespace-nowrap">
|
||||
<span className="text-sm text-[var(--text-secondary)] whitespace-nowrap">
|
||||
{product.unit || 'kg'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -221,10 +236,10 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
|
||||
{finishedProducts.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<Package className="w-4 h-4 text-blue-600" />
|
||||
<div className="w-8 h-8 bg-[var(--color-info)]/10 dark:bg-[var(--color-info)]/20 rounded-lg flex items-center justify-center">
|
||||
<Package className="w-4 h-4 text-[var(--color-info)]" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-text-primary">
|
||||
<h3 className="font-semibold text-[var(--text-primary)]">
|
||||
{t('onboarding:stock.finished_products', 'Productos Terminados')} ({finishedProducts.length})
|
||||
</h3>
|
||||
</div>
|
||||
@@ -233,16 +248,16 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
|
||||
{finishedProducts.map(product => {
|
||||
const hasStock = product.initialStock !== undefined;
|
||||
return (
|
||||
<Card key={product.id} className={hasStock ? 'bg-blue-50 border-blue-200' : ''}>
|
||||
<Card key={product.id} className={hasStock ? 'bg-[var(--color-info)]/10 dark:bg-[var(--color-info)]/20 border-[var(--color-info)]/30' : ''}>
|
||||
<div className="p-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-text-primary flex items-center gap-2">
|
||||
<div className="font-medium text-[var(--text-primary)] flex items-center gap-2">
|
||||
{product.name}
|
||||
{hasStock && <CheckCircle className="w-4 h-4 text-blue-600" />}
|
||||
{hasStock && <CheckCircle className="w-4 h-4 text-[var(--color-info)]" />}
|
||||
</div>
|
||||
{product.category && (
|
||||
<div className="text-xs text-text-secondary">{product.category}</div>
|
||||
<div className="text-xs text-[var(--text-secondary)]">{product.category}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -255,7 +270,7 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
|
||||
step="1"
|
||||
className="w-24 text-right"
|
||||
/>
|
||||
<span className="text-sm text-text-secondary whitespace-nowrap">
|
||||
<span className="text-sm text-[var(--text-secondary)] whitespace-nowrap">
|
||||
{product.unit || t('common:units', 'unidades')}
|
||||
</span>
|
||||
</div>
|
||||
@@ -270,35 +285,34 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
|
||||
|
||||
{/* Warning for incomplete */}
|
||||
{!allCompleted && (
|
||||
<Card className="bg-amber-50 border-amber-200">
|
||||
<div className="p-4 flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-amber-900">
|
||||
<p className="font-medium">
|
||||
{t('onboarding:stock.incomplete_warning', 'Faltan {count} productos por completar', {
|
||||
<InfoCard
|
||||
variant="warning"
|
||||
title={t('onboarding:stock.incomplete_warning', 'Faltan {{count}} productos por completar', {
|
||||
count: productsWithoutStock.length,
|
||||
})}
|
||||
</p>
|
||||
<p className="text-amber-700 mt-1">
|
||||
{t(
|
||||
description={t(
|
||||
'onboarding:stock.incomplete_help',
|
||||
'Puedes continuar, pero recomendamos ingresar todas las cantidades para un mejor control de inventario.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="flex items-center justify-between pt-6 border-t border-border-primary">
|
||||
<div className="flex items-center justify-between pt-6 border-t border-[var(--border-primary)]">
|
||||
<Button onClick={onPrevious} variant="ghost" leftIcon={<ArrowLeft />}>
|
||||
{t('common:previous', 'Anterior')}
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleContinue} variant="primary" rightIcon={<ArrowRight />}>
|
||||
{allCompleted
|
||||
? t('onboarding:stock.complete', 'Completar Configuración')
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
variant="primary"
|
||||
rightIcon={<ArrowRight />}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving
|
||||
? t('common:saving', 'Guardando...')
|
||||
: allCompleted
|
||||
? t('onboarding:stock.continue_to_next', 'Continuar →')
|
||||
: t('onboarding:stock.continue_anyway', 'Continuar de todos modos')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRegisterBakery } from '../../../../api/hooks/tenant';
|
||||
import { BakeryRegistration } from '../../../../api/types/tenant';
|
||||
import { AddressResult } from '../../../../services/api/geocodingApi';
|
||||
import { useWizardContext } from '../context';
|
||||
import { poiContextApi } from '../../../../services/api/poiContextApi';
|
||||
|
||||
interface RegisterTenantStepProps {
|
||||
onNext: () => void;
|
||||
@@ -112,8 +113,25 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
try {
|
||||
const tenant = await registerBakery.mutateAsync(formData);
|
||||
|
||||
// Update the wizard context with tenant info and pass the bakeryLocation coordinates
|
||||
// that were captured during address selection to the next step (POI Detection)
|
||||
// Trigger POI detection in the background (non-blocking)
|
||||
// This replaces the removed POI Detection step
|
||||
const bakeryLocation = wizardContext.state.bakeryLocation;
|
||||
if (bakeryLocation?.latitude && bakeryLocation?.longitude && tenant.id) {
|
||||
// Run POI detection asynchronously without blocking the wizard flow
|
||||
poiContextApi.detectPOIs(
|
||||
tenant.id,
|
||||
bakeryLocation.latitude,
|
||||
bakeryLocation.longitude,
|
||||
false // use_cache = false for initial detection
|
||||
).then((result) => {
|
||||
console.log(`✅ POI detection completed automatically for tenant ${tenant.id}:`, result.summary);
|
||||
}).catch((error) => {
|
||||
console.warn('⚠️ Background POI detection failed (non-blocking):', error);
|
||||
// This is non-critical, so we don't block the user
|
||||
});
|
||||
}
|
||||
|
||||
// Update the wizard context with tenant info
|
||||
onComplete({
|
||||
tenant,
|
||||
tenantId: tenant.id,
|
||||
|
||||
@@ -274,10 +274,10 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
console.log('Check type clicked:', option.value, 'current:', formData.check_type);
|
||||
setFormData(prev => ({ ...prev, check_type: option.value }));
|
||||
}}
|
||||
className={`p-3 text-left border rounded-lg transition-colors cursor-pointer ${
|
||||
className={`p-3 text-left border-2 rounded-lg transition-all cursor-pointer ${
|
||||
formData.check_type === option.value
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/10'
|
||||
: 'border-[var(--border-secondary)] hover:border-[var(--border-primary)]'
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/20 shadow-lg ring-2 ring-[var(--color-primary)]/30'
|
||||
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
|
||||
}`}
|
||||
>
|
||||
<div className="text-lg mb-1">{option.icon}</div>
|
||||
@@ -325,10 +325,10 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
: [...prev.applicable_stages, option.value]
|
||||
}));
|
||||
}}
|
||||
className={`p-2 text-sm text-left border rounded-lg transition-colors cursor-pointer ${
|
||||
className={`p-2 text-sm text-left border-2 rounded-lg transition-all cursor-pointer ${
|
||||
formData.applicable_stages.includes(option.value)
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/10 text-[var(--color-primary)]'
|
||||
: 'border-[var(--border-secondary)] text-[var(--text-secondary)] hover:border-[var(--border-primary)]'
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/20 text-[var(--color-primary)] font-semibold shadow-md ring-1 ring-[var(--color-primary)]/30'
|
||||
: 'border-[var(--border-secondary)] text-[var(--text-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
|
||||
@@ -565,7 +565,9 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
className={`w-full px-3 py-2 bg-[var(--bg-primary)] border ${errors.finished_product_id ? 'border-[var(--color-error)]' : 'border-[var(--border-secondary)]'} rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]`}
|
||||
>
|
||||
<option value="">{t('setup_wizard:recipes.placeholders.finished_product', 'Select finished product...')}</option>
|
||||
{ingredients.map((ing) => (
|
||||
{ingredients
|
||||
.filter((ing) => ing.product_type === 'finished_product')
|
||||
.map((ing) => (
|
||||
<option key={ing.id} value={ing.id}>
|
||||
{ing.name} ({ing.unit_of_measure})
|
||||
</option>
|
||||
@@ -793,19 +795,6 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
tenantId={tenantId}
|
||||
context="recipe"
|
||||
/>
|
||||
|
||||
{/* Continue button - only shown when used in onboarding context */}
|
||||
{onComplete && (
|
||||
<div className="flex justify-end mt-6 pt-6 border-[var(--border-secondary)]">
|
||||
<button
|
||||
onClick={() => onComplete()}
|
||||
disabled={canContinue === false}
|
||||
className="px-6 py-3 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
|
||||
>
|
||||
{t('setup_wizard:navigation.continue', 'Continue →')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -247,10 +247,10 @@ export const TeamSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplete,
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, role: option.value })}
|
||||
className={`p-3 text-left border rounded-lg transition-colors ${
|
||||
className={`p-3 text-left border-2 rounded-lg transition-all ${
|
||||
formData.role === option.value
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/10'
|
||||
: 'border-[var(--border-secondary)] hover:border-[var(--border-primary)]'
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/20 shadow-lg ring-2 ring-[var(--color-primary)]/30'
|
||||
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
|
||||
@@ -99,7 +99,7 @@ export const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
|
||||
return (
|
||||
<div ref={wrapperRef} className={`relative ${className}`}>
|
||||
<div className="relative">
|
||||
<MapPin className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<MapPin className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-[var(--text-secondary)]" />
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
@@ -109,15 +109,15 @@ export const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
className={`pl-10 pr-10 ${selectedAddress ? 'border-green-500' : ''}`}
|
||||
className={`pl-10 pr-10 ${selectedAddress ? 'border-[var(--color-success)]' : ''}`}
|
||||
/>
|
||||
|
||||
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 flex items-center gap-1">
|
||||
{isLoading && (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-gray-400" />
|
||||
<Loader2 className="h-4 w-4 animate-spin text-[var(--text-secondary)]" />
|
||||
)}
|
||||
{selectedAddress && !isLoading && (
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
<Check className="h-4 w-4 text-[var(--color-success)]" />
|
||||
)}
|
||||
{query && !disabled && (
|
||||
<Button
|
||||
@@ -125,7 +125,7 @@ export const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleClear}
|
||||
className="h-6 w-6 p-0 hover:bg-gray-100"
|
||||
className="h-6 w-6 p-0 hover:bg-[var(--bg-secondary)]"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -135,36 +135,36 @@ export const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<div className="mt-1 text-sm text-red-600">
|
||||
<div className="mt-1 text-sm text-[var(--color-error)]">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Results dropdown */}
|
||||
{showResults && results.length > 0 && (
|
||||
<Card className="absolute z-50 w-full mt-1 max-h-80 overflow-y-auto shadow-lg">
|
||||
<Card className="absolute z-50 w-full mt-1 max-h-80 overflow-y-auto shadow-lg bg-[var(--bg-primary)]">
|
||||
<CardBody className="p-0">
|
||||
<div className="divide-y divide-gray-100">
|
||||
<div className="divide-y divide-[var(--border-secondary)]">
|
||||
{results.map((result) => (
|
||||
<button
|
||||
key={result.place_id}
|
||||
type="button"
|
||||
onClick={() => handleSelectAddress(result)}
|
||||
className="w-full text-left px-4 py-3 hover:bg-gray-50 focus:bg-gray-50 focus:outline-none transition-colors"
|
||||
className="w-full text-left px-4 py-3 hover:bg-[var(--bg-secondary)] focus:bg-[var(--bg-secondary)] focus:outline-none transition-colors"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<MapPin className="h-4 w-4 text-blue-600 mt-1 flex-shrink-0" />
|
||||
<MapPin className="h-4 w-4 text-[var(--color-primary)] mt-1 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-gray-900 truncate">
|
||||
<div className="text-sm font-medium text-[var(--text-primary)] truncate">
|
||||
{result.address.road && result.address.house_number
|
||||
? `${result.address.road}, ${result.address.house_number}`
|
||||
: result.address.road || result.display_name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 truncate mt-0.5">
|
||||
<div className="text-xs text-[var(--text-secondary)] truncate mt-0.5">
|
||||
{result.address.city || result.address.municipality || result.address.suburb}
|
||||
{result.address.postcode && `, ${result.address.postcode}`}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
<div className="text-xs text-[var(--text-tertiary)] mt-1">
|
||||
{result.lat.toFixed(6)}, {result.lon.toFixed(6)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -178,9 +178,9 @@ export const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
|
||||
|
||||
{/* No results message */}
|
||||
{showResults && !isLoading && query.length >= 3 && results.length === 0 && !error && (
|
||||
<Card className="absolute z-50 w-full mt-1 shadow-lg">
|
||||
<Card className="absolute z-50 w-full mt-1 shadow-lg bg-[var(--bg-primary)]">
|
||||
<CardBody className="p-4">
|
||||
<div className="text-sm text-gray-600 text-center">
|
||||
<div className="text-sm text-[var(--text-secondary)] text-center">
|
||||
No addresses found for "{query}"
|
||||
</div>
|
||||
</CardBody>
|
||||
|
||||
92
frontend/src/components/ui/InfoCard.tsx
Normal file
92
frontend/src/components/ui/InfoCard.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { AlertCircle, Info, Lightbulb, Sparkles } from 'lucide-react';
|
||||
|
||||
export type InfoCardVariant = 'info' | 'warning' | 'success' | 'tip' | 'template';
|
||||
|
||||
interface InfoCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
variant?: InfoCardVariant;
|
||||
icon?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified InfoCard component for displaying consistent info/warning/tip blocks
|
||||
* across all onboarding wizard steps.
|
||||
*
|
||||
* Uses global styling and color palette for dark mode compatibility.
|
||||
*/
|
||||
export const InfoCard: React.FC<InfoCardProps> = ({
|
||||
title,
|
||||
description,
|
||||
variant = 'info',
|
||||
icon,
|
||||
children,
|
||||
className = '',
|
||||
}) => {
|
||||
const getVariantStyles = () => {
|
||||
switch (variant) {
|
||||
case 'info':
|
||||
return {
|
||||
container: 'bg-[var(--color-info)]/10 border-[var(--color-info)]/20',
|
||||
icon: 'text-[var(--color-info)]',
|
||||
defaultIcon: <Info className="w-5 h-5" />,
|
||||
};
|
||||
case 'warning':
|
||||
return {
|
||||
container: 'bg-[var(--color-warning)]/10 border-[var(--color-warning)]/20',
|
||||
icon: 'text-[var(--color-warning)]',
|
||||
defaultIcon: <AlertCircle className="w-5 h-5" />,
|
||||
};
|
||||
case 'success':
|
||||
return {
|
||||
container: 'bg-[var(--color-success)]/10 border-[var(--color-success)]/20',
|
||||
icon: 'text-[var(--color-success)]',
|
||||
defaultIcon: <Sparkles className="w-5 h-5" />,
|
||||
};
|
||||
case 'tip':
|
||||
return {
|
||||
container: 'bg-[var(--color-primary)]/10 border-[var(--color-primary)]/20',
|
||||
icon: 'text-[var(--color-primary)]',
|
||||
defaultIcon: <Lightbulb className="w-5 h-5" />,
|
||||
};
|
||||
case 'template':
|
||||
return {
|
||||
container: 'bg-gradient-to-br from-purple-50 to-blue-50 dark:from-purple-900/10 dark:to-blue-900/10 border-purple-200 dark:border-purple-700',
|
||||
icon: 'text-purple-600 dark:text-purple-400',
|
||||
defaultIcon: <Sparkles className="w-5 h-5" />,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
container: 'bg-[var(--color-info)]/10 border-[var(--color-info)]/20',
|
||||
icon: 'text-[var(--color-info)]',
|
||||
defaultIcon: <Info className="w-5 h-5" />,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const styles = getVariantStyles();
|
||||
|
||||
return (
|
||||
<div className={`${styles.container} border rounded-lg p-4 ${className}`}>
|
||||
<h3 className="font-semibold text-[var(--text-primary)] mb-2 flex items-center gap-2">
|
||||
<span className={styles.icon}>
|
||||
{icon || styles.defaultIcon}
|
||||
</span>
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{description}
|
||||
</p>
|
||||
{children && (
|
||||
<div className="mt-3">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfoCard;
|
||||
55
frontend/src/components/ui/TemplateCard.tsx
Normal file
55
frontend/src/components/ui/TemplateCard.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
|
||||
interface TemplateCardProps {
|
||||
icon: string | React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
itemsCount?: number;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified TemplateCard component for displaying template options
|
||||
* across all onboarding wizard steps.
|
||||
*
|
||||
* Uses global styling and color palette for dark mode compatibility.
|
||||
*/
|
||||
export const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
itemsCount,
|
||||
onClick,
|
||||
className = '',
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`text-left p-4 bg-[var(--bg-primary)] dark:bg-[var(--surface-secondary)] border-2 border-purple-200 dark:border-purple-700 rounded-lg hover:border-purple-400 dark:hover:border-purple-500 hover:shadow-md transition-all group ${className}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{typeof icon === 'string' ? (
|
||||
<span className="text-3xl group-hover:scale-110 transition-transform">{icon}</span>
|
||||
) : (
|
||||
<span className="group-hover:scale-110 transition-transform">{icon}</span>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-1">
|
||||
{title}
|
||||
</h4>
|
||||
<p className="text-xs text-[var(--text-secondary)] mb-2">
|
||||
{description}
|
||||
</p>
|
||||
{itemsCount !== undefined && (
|
||||
<p className="text-xs text-purple-600 dark:text-purple-400 font-medium">
|
||||
{itemsCount} items
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateCard;
|
||||
@@ -132,6 +132,30 @@
|
||||
"last_30_days": "Last 30 days",
|
||||
"last_90_days": "Last 90 days"
|
||||
},
|
||||
"config": {
|
||||
"title": "Complete Your Bakery Setup",
|
||||
"subtitle": "Configure essential features to get started",
|
||||
"inventory": "Inventory",
|
||||
"suppliers": "Suppliers",
|
||||
"recipes": "Recipes",
|
||||
"quality": "Quality Standards",
|
||||
"add_ingredients": "Add at least {{count}} ingredients",
|
||||
"add_supplier": "Add your first supplier",
|
||||
"add_recipe": "Create your first recipe",
|
||||
"add_quality": "Add quality checks (optional)",
|
||||
"sections_complete": "sections complete",
|
||||
"added": "added",
|
||||
"recommended": "recommended",
|
||||
"next_step": "Next Step",
|
||||
"configure": "Configure",
|
||||
"features_unlocked": "Features Unlocked!",
|
||||
"features": {
|
||||
"inventory_tracking": "Inventory Tracking",
|
||||
"purchase_orders": "Purchase Orders",
|
||||
"production_planning": "Production Planning",
|
||||
"cost_analysis": "Cost Analysis"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"failed_to_load_stats": "Failed to load dashboard statistics. Please try again."
|
||||
}
|
||||
|
||||
276
frontend/src/locales/en/setup_wizard.json
Normal file
276
frontend/src/locales/en/setup_wizard.json
Normal file
@@ -0,0 +1,276 @@
|
||||
{
|
||||
"why_this_matters": "Why This Matters",
|
||||
"optional": "Optional",
|
||||
"navigation": {
|
||||
"continue": "Continue →",
|
||||
"back": "← Back",
|
||||
"skip": "Skip for now"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Excellent! Your AI is Ready",
|
||||
"subtitle": "Now let's set up your bakery's daily operations so the system can help you manage:",
|
||||
"feature_inventory": "Inventory Tracking",
|
||||
"feature_inventory_desc": "Real-time stock levels & reorder alerts",
|
||||
"feature_recipes": "Recipe Costing",
|
||||
"feature_recipes_desc": "Automatic cost calculation & profitability analysis",
|
||||
"feature_quality": "Quality Monitoring",
|
||||
"feature_quality_desc": "Track standards & production quality",
|
||||
"feature_team": "Team Coordination",
|
||||
"feature_team_desc": "Assign tasks & track responsibilities",
|
||||
"time_estimate": "Takes about 15-20 minutes",
|
||||
"save_resume": "You can save progress and resume anytime",
|
||||
"skip": "I'll Do This Later",
|
||||
"get_started": "Let's Get Started! →"
|
||||
},
|
||||
"suppliers": {
|
||||
"why": "Suppliers are the source of your ingredients. Setting them up now lets you track costs, manage orders, and analyze supplier performance.",
|
||||
"added_count": "{{count}} supplier added",
|
||||
"added_count_plural": "{{count}} suppliers added",
|
||||
"minimum_met": "Minimum requirement met",
|
||||
"add_minimum": "Add at least 1 supplier to continue",
|
||||
"your_suppliers": "Your Suppliers",
|
||||
"confirm_delete": "Are you sure you want to delete this supplier?",
|
||||
"edit_supplier": "Edit Supplier",
|
||||
"add_supplier": "Add Supplier",
|
||||
"add_first": "Add Your First Supplier",
|
||||
"add_another": "Add Another Supplier",
|
||||
"manage_products": "Manage Products",
|
||||
"products": "products",
|
||||
"products_for": "Products for {{name}}",
|
||||
"add_products": "Add Products",
|
||||
"no_products_available": "No products available",
|
||||
"select_products": "Select Products",
|
||||
"unit_price": "Price",
|
||||
"unit": "Unit",
|
||||
"min_qty": "Min Qty",
|
||||
"add_new_product": "Add New Product",
|
||||
"save_products": "Save",
|
||||
"no_products_warning": "Add at least 1 product to enable automatic purchase orders",
|
||||
"fields": {
|
||||
"name": "Supplier Name",
|
||||
"type": "Type",
|
||||
"contact_person": "Contact Person",
|
||||
"phone": "Phone",
|
||||
"email": "Email"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "e.g., Molinos SA, Distribuidora López",
|
||||
"contact_person": "e.g., Juan Pérez",
|
||||
"phone": "e.g., +54 11 1234-5678",
|
||||
"email": "e.g., ventas@proveedor.com"
|
||||
},
|
||||
"errors": {
|
||||
"name_required": "Name is required",
|
||||
"email_invalid": "Invalid email format"
|
||||
}
|
||||
},
|
||||
"inventory": {
|
||||
"why": "Inventory items are the building blocks of your recipes. Once set up, the system will track quantities, alert you when stock is low, and help you calculate recipe costs.",
|
||||
"quick_start": "Quick Start",
|
||||
"quick_start_desc": "Import common ingredients to get started quickly",
|
||||
"essential": "Essential Ingredients",
|
||||
"common": "Common Ingredients",
|
||||
"packaging": "Packaging",
|
||||
"import_all": "Import All",
|
||||
"templates_hint": "Click any item to customize before adding, or use \"Import All\" for quick setup",
|
||||
"show_templates": "Show Quick Start Templates",
|
||||
"added_count": "{{count}} ingredient added",
|
||||
"added_count_plural": "{{count}} ingredients added",
|
||||
"minimum_met": "Minimum requirement met",
|
||||
"need_more": "Need {{count}} more",
|
||||
"your_ingredients": "Your Ingredients",
|
||||
"add_ingredient": "Add Ingredient",
|
||||
"edit_ingredient": "Edit Ingredient",
|
||||
"add_first": "Add Your First Ingredient",
|
||||
"add_another": "Add Another Ingredient",
|
||||
"confirm_delete": "Are you sure you want to delete this ingredient?",
|
||||
"add_stock": "Add Initial Stock",
|
||||
"quantity": "Quantity",
|
||||
"expiration_date": "Expiration Date",
|
||||
"supplier": "Supplier",
|
||||
"batch_number": "Batch/Lot Number",
|
||||
"stock_help": "Expiration tracking helps prevent waste and enables FIFO inventory management",
|
||||
"add_another_lot": "+ Add Another Lot",
|
||||
"add_another_stock": "Add Another Stock Lot",
|
||||
"add_initial_stock": "Add Initial Stock (Optional)",
|
||||
"fields": {
|
||||
"name": "Ingredient Name",
|
||||
"category": "Category",
|
||||
"unit": "Unit of Measure",
|
||||
"brand": "Brand",
|
||||
"cost": "Standard Cost"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "e.g., Harina 000, Levadura fresca",
|
||||
"brand": "e.g., Molinos Río",
|
||||
"cost": "e.g., 150.00"
|
||||
},
|
||||
"errors": {
|
||||
"name_required": "Name is required",
|
||||
"cost_invalid": "Cost must be a valid number",
|
||||
"threshold_invalid": "Threshold must be a valid number"
|
||||
},
|
||||
"stock_errors": {
|
||||
"quantity_required": "Quantity must be greater than zero",
|
||||
"expiration_past": "Expiration date is in the past",
|
||||
"expiring_soon": "Warning: This ingredient expires very soon!"
|
||||
}
|
||||
},
|
||||
"recipes": {
|
||||
"why": "Recipes connect your inventory to production. The system will calculate exact costs per item, track ingredient consumption, and help you optimize your menu profitability.",
|
||||
"quick_start": "Recipe Templates",
|
||||
"quick_start_desc": "Start with proven recipes and customize to your needs",
|
||||
"category": {
|
||||
"breads": "Breads",
|
||||
"pastries": "Pastries",
|
||||
"cakes": "Cakes & Tarts",
|
||||
"cookies": "Cookies"
|
||||
},
|
||||
"use_template": "Use Template",
|
||||
"templates_hint": "Templates will automatically match your ingredients. Review and adjust as needed.",
|
||||
"show_templates": "Show Recipe Templates",
|
||||
"prerequisites_title": "More ingredients needed",
|
||||
"prerequisites_desc": "You need at least 2 ingredients in your inventory before creating recipes. Go back to the Inventory step to add more ingredients.",
|
||||
"added_count": "{{count}} recipe added",
|
||||
"added_count_plural": "{{count}} recipes added",
|
||||
"minimum_met": "{{count}} recipe(s) added - Ready to continue!",
|
||||
"your_recipes": "Your Recipes",
|
||||
"yield_label": "Yield",
|
||||
"add_recipe": "Add Recipe",
|
||||
"add_first": "Add Your First Recipe",
|
||||
"add_another": "Add Another Recipe",
|
||||
"add_new_ingredient": "Add New Ingredient",
|
||||
"select_ingredient": "Select...",
|
||||
"add_ingredient": "Add Ingredient",
|
||||
"no_ingredients": "No ingredients added yet",
|
||||
"confirm_delete": "Are you sure you want to delete this recipe?",
|
||||
"fields": {
|
||||
"name": "Recipe Name",
|
||||
"finished_product": "Finished Product",
|
||||
"yield_quantity": "Yield Quantity",
|
||||
"yield_unit": "Unit",
|
||||
"ingredients": "Ingredients"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "e.g., Baguette, Croissant",
|
||||
"finished_product": "Select finished product..."
|
||||
},
|
||||
"errors": {
|
||||
"name_required": "Recipe name is required",
|
||||
"finished_product_required": "Finished product is required",
|
||||
"yield_invalid": "Yield must be a positive number",
|
||||
"ingredients_required": "At least one ingredient is required",
|
||||
"ingredient_required": "Ingredient is required",
|
||||
"quantity_invalid": "Quantity must be positive"
|
||||
}
|
||||
},
|
||||
"quality": {
|
||||
"why": "Quality checks ensure consistent output and help you identify issues early. Define what \"good\" looks like for each stage of production.",
|
||||
"optional_note": "You can skip this and configure quality checks later",
|
||||
"added_count": "{{count}} quality check added",
|
||||
"added_count_plural": "{{count}} quality checks added",
|
||||
"recommended_met": "Recommended amount met",
|
||||
"recommended": "2+ recommended (optional)",
|
||||
"your_checks": "Your Quality Checks",
|
||||
"add_check": "Add Quality Check",
|
||||
"add_first": "Add Your First Quality Check",
|
||||
"add_another": "Add Another Quality Check",
|
||||
"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)"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "e.g., Crust color check, Dough temperature",
|
||||
"description": "What should be checked and why..."
|
||||
},
|
||||
"errors": {
|
||||
"name_required": "Name is required",
|
||||
"stages_required": "At least one stage is required"
|
||||
}
|
||||
},
|
||||
"team": {
|
||||
"why": "Adding team members allows you to assign tasks, track who does what, and give everyone the tools they need to work efficiently.",
|
||||
"optional_note": "You can add team members now or invite them later from settings",
|
||||
"invitation_note": "Team members will receive invitation emails once you complete the setup wizard.",
|
||||
"added_count": "{{count}} team member added",
|
||||
"added_count_plural": "{{count}} team members added",
|
||||
"your_team": "Your Team Members",
|
||||
"add_member": "Add Team Member",
|
||||
"add_first": "Add Your First Team Member",
|
||||
"add_another": "Add Another Team Member",
|
||||
"skip_message": "Working alone for now? No problem!",
|
||||
"skip_hint": "You can always invite team members later from Settings → Team",
|
||||
"fields": {
|
||||
"name": "Full Name",
|
||||
"email": "Email Address",
|
||||
"role": "Role"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "e.g., María García",
|
||||
"email": "e.g., maria@panaderia.com"
|
||||
},
|
||||
"errors": {
|
||||
"name_required": "Name is required",
|
||||
"email_required": "Email is required",
|
||||
"email_invalid": "Invalid email format",
|
||||
"email_duplicate": "This email is already added"
|
||||
}
|
||||
},
|
||||
"review": {
|
||||
"title": "Review Your Setup",
|
||||
"subtitle": "Let's review everything you've configured. You can go back and make changes if needed.",
|
||||
"suppliers": "Suppliers",
|
||||
"ingredients": "Ingredients",
|
||||
"recipes": "Recipes",
|
||||
"quality": "Quality Checks",
|
||||
"suppliers_title": "Suppliers",
|
||||
"more": "more",
|
||||
"ingredients_title": "Inventory Items",
|
||||
"total_cost": "Total value",
|
||||
"recipes_title": "Recipes",
|
||||
"avg_ingredients": "Avg ingredients",
|
||||
"yields": "Yields",
|
||||
"cost": "Cost",
|
||||
"quality_title": "Quality Check Templates",
|
||||
"required": "Required",
|
||||
"ready_title": "Your Bakery is Ready to Go!",
|
||||
"ready_message": "You've successfully configured {{suppliers}} suppliers, {{ingredients}} ingredients, and {{recipes}} recipes. Click 'Complete Setup' to finish and start using the system.",
|
||||
"help": "Need to make changes? Use the \"Back\" button to return to any step."
|
||||
},
|
||||
"completion": {
|
||||
"title": "🎉 Setup Complete!",
|
||||
"subtitle": "Congratulations! Your bakery management system is ready to use. Let's get started with your first tasks.",
|
||||
"next_steps": "Recommended Next Steps",
|
||||
"step1_title": "Start Production",
|
||||
"step1_desc": "Create your first production batch using your configured recipes",
|
||||
"step1_action": "Go to Production",
|
||||
"step2_title": "Order Inventory",
|
||||
"step2_desc": "Place your first purchase order with your suppliers",
|
||||
"step2_action": "View Procurement",
|
||||
"step3_title": "Track Analytics",
|
||||
"step3_desc": "Monitor your production efficiency and costs in real-time",
|
||||
"step3_action": "View Analytics",
|
||||
"tips": "Pro Tips for Success",
|
||||
"tip1_title": "Keep Inventory Updated",
|
||||
"tip1_desc": "Regularly update stock levels to get accurate cost calculations and low-stock alerts",
|
||||
"tip2_title": "Monitor Quality Metrics",
|
||||
"tip2_desc": "Use quality checks during production to identify issues early and maintain consistency",
|
||||
"tip3_title": "Review Analytics Weekly",
|
||||
"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",
|
||||
"dashboard_desc": "View overview",
|
||||
"recipes": "Recipes",
|
||||
"recipes_desc": "Manage recipes",
|
||||
"go_dashboard": "Go to Dashboard",
|
||||
"thanks": "Thank you for completing the setup! Happy baking! 🥖🥐🍰"
|
||||
}
|
||||
}
|
||||
@@ -167,6 +167,30 @@
|
||||
"last_30_days": "Últimos 30 días",
|
||||
"last_90_days": "Últimos 90 días"
|
||||
},
|
||||
"config": {
|
||||
"title": "Completa la Configuración de tu Panadería",
|
||||
"subtitle": "Configura características esenciales para comenzar",
|
||||
"inventory": "Inventario",
|
||||
"suppliers": "Proveedores",
|
||||
"recipes": "Recetas",
|
||||
"quality": "Estándares de Calidad",
|
||||
"add_ingredients": "Agregar al menos {{count}} ingredientes",
|
||||
"add_supplier": "Agregar tu primer proveedor",
|
||||
"add_recipe": "Crear tu primera receta",
|
||||
"add_quality": "Agregar controles de calidad (opcional)",
|
||||
"sections_complete": "secciones completas",
|
||||
"added": "agregado",
|
||||
"recommended": "recomendado",
|
||||
"next_step": "Siguiente Paso",
|
||||
"configure": "Configurar",
|
||||
"features_unlocked": "¡Características Desbloqueadas!",
|
||||
"features": {
|
||||
"inventory_tracking": "Seguimiento de Inventario",
|
||||
"purchase_orders": "Órdenes de Compra",
|
||||
"production_planning": "Planificación de Producción",
|
||||
"cost_analysis": "Análisis de Costos"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"failed_to_load_stats": "Error al cargar las estadísticas del panel. Por favor, inténtelo de nuevo."
|
||||
}
|
||||
|
||||
276
frontend/src/locales/es/setup_wizard.json
Normal file
276
frontend/src/locales/es/setup_wizard.json
Normal file
@@ -0,0 +1,276 @@
|
||||
{
|
||||
"why_this_matters": "Por qué es importante",
|
||||
"optional": "Opcional",
|
||||
"navigation": {
|
||||
"continue": "Continuar →",
|
||||
"back": "← Atrás",
|
||||
"skip": "Omitir por ahora"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "¡Excelente! Tu IA está lista",
|
||||
"subtitle": "Ahora configuremos las operaciones diarias de tu panadería para que el sistema pueda ayudarte a gestionar:",
|
||||
"feature_inventory": "Control de Inventario",
|
||||
"feature_inventory_desc": "Niveles de stock en tiempo real y alertas de reposición",
|
||||
"feature_recipes": "Costeo de Recetas",
|
||||
"feature_recipes_desc": "Cálculo automático de costos y análisis de rentabilidad",
|
||||
"feature_quality": "Monitoreo de Calidad",
|
||||
"feature_quality_desc": "Seguimiento de estándares y calidad de producción",
|
||||
"feature_team": "Coordinación del Equipo",
|
||||
"feature_team_desc": "Asignar tareas y seguir responsabilidades",
|
||||
"time_estimate": "Toma aproximadamente 15-20 minutos",
|
||||
"save_resume": "Puedes guardar el progreso y reanudar en cualquier momento",
|
||||
"skip": "Lo haré más tarde",
|
||||
"get_started": "¡Empecemos! →"
|
||||
},
|
||||
"suppliers": {
|
||||
"why": "Los proveedores son la fuente de tus ingredientes. Configurarlos ahora te permite rastrear costos, gestionar pedidos y analizar el rendimiento de los proveedores.",
|
||||
"added_count": "{{count}} proveedor agregado",
|
||||
"added_count_plural": "{{count}} proveedores agregados",
|
||||
"minimum_met": "Requisito mínimo cumplido",
|
||||
"add_minimum": "Agrega al menos 1 proveedor para continuar",
|
||||
"your_suppliers": "Tus Proveedores",
|
||||
"confirm_delete": "¿Estás seguro de que deseas eliminar este proveedor?",
|
||||
"edit_supplier": "Editar Proveedor",
|
||||
"add_supplier": "Agregar Proveedor",
|
||||
"add_first": "Agrega tu Primer Proveedor",
|
||||
"add_another": "Agregar Otro Proveedor",
|
||||
"manage_products": "Gestionar Productos",
|
||||
"products": "productos",
|
||||
"products_for": "Productos para {{name}}",
|
||||
"add_products": "Agregar Productos",
|
||||
"no_products_available": "No hay productos disponibles",
|
||||
"select_products": "Seleccionar Productos",
|
||||
"unit_price": "Precio",
|
||||
"unit": "Unidad",
|
||||
"min_qty": "Cant. Mín.",
|
||||
"add_new_product": "Agregar Nuevo Producto",
|
||||
"save_products": "Guardar",
|
||||
"no_products_warning": "Agrega al menos 1 producto para habilitar órdenes de compra automáticas",
|
||||
"fields": {
|
||||
"name": "Nombre del Proveedor",
|
||||
"type": "Tipo",
|
||||
"contact_person": "Persona de Contacto",
|
||||
"phone": "Teléfono",
|
||||
"email": "Correo Electrónico"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "ej., Molinos SA, Distribuidora López",
|
||||
"contact_person": "ej., Juan Pérez",
|
||||
"phone": "ej., +34 91 123 4567",
|
||||
"email": "ej., ventas@proveedor.com"
|
||||
},
|
||||
"errors": {
|
||||
"name_required": "El nombre es obligatorio",
|
||||
"email_invalid": "Formato de correo inválido"
|
||||
}
|
||||
},
|
||||
"inventory": {
|
||||
"why": "Los artículos de inventario son los componentes básicos de tus recetas. Una vez configurados, el sistema rastreará las cantidades, te alertará cuando el stock sea bajo y te ayudará a calcular los costos de las recetas.",
|
||||
"quick_start": "Inicio Rápido",
|
||||
"quick_start_desc": "Importa ingredientes comunes para comenzar rápidamente",
|
||||
"essential": "Ingredientes Esenciales",
|
||||
"common": "Ingredientes Comunes",
|
||||
"packaging": "Embalaje",
|
||||
"import_all": "Importar Todo",
|
||||
"templates_hint": "Haz clic en cualquier artículo para personalizarlo antes de agregarlo, o usa \"Importar Todo\" para una configuración rápida",
|
||||
"show_templates": "Mostrar Plantillas de Inicio Rápido",
|
||||
"added_count": "{{count}} ingrediente agregado",
|
||||
"added_count_plural": "{{count}} ingredientes agregados",
|
||||
"minimum_met": "Requisito mínimo cumplido",
|
||||
"need_more": "Necesitas {{count}} más",
|
||||
"your_ingredients": "Tus Ingredientes",
|
||||
"add_ingredient": "Agregar Ingrediente",
|
||||
"edit_ingredient": "Editar Ingrediente",
|
||||
"add_first": "Agrega tu Primer Ingrediente",
|
||||
"add_another": "Agregar Otro Ingrediente",
|
||||
"confirm_delete": "¿Estás seguro de que deseas eliminar este ingrediente?",
|
||||
"add_stock": "Agregar Stock Inicial",
|
||||
"quantity": "Cantidad",
|
||||
"expiration_date": "Fecha de Vencimiento",
|
||||
"supplier": "Proveedor",
|
||||
"batch_number": "Número de Lote",
|
||||
"stock_help": "El seguimiento de vencimiento ayuda a prevenir desperdicios y habilita la gestión de inventario FIFO",
|
||||
"add_another_lot": "+ Agregar Otro Lote",
|
||||
"add_another_stock": "Agregar Otro Lote de Stock",
|
||||
"add_initial_stock": "Agregar Stock Inicial (Opcional)",
|
||||
"fields": {
|
||||
"name": "Nombre del Ingrediente",
|
||||
"category": "Categoría",
|
||||
"unit": "Unidad de Medida",
|
||||
"brand": "Marca",
|
||||
"cost": "Costo Estándar"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "ej., Harina 000, Levadura fresca",
|
||||
"brand": "ej., Molinos Río",
|
||||
"cost": "ej., 150.00"
|
||||
},
|
||||
"errors": {
|
||||
"name_required": "El nombre es obligatorio",
|
||||
"cost_invalid": "El costo debe ser un número válido",
|
||||
"threshold_invalid": "El umbral debe ser un número válido"
|
||||
},
|
||||
"stock_errors": {
|
||||
"quantity_required": "La cantidad debe ser mayor que cero",
|
||||
"expiration_past": "La fecha de vencimiento está en el pasado",
|
||||
"expiring_soon": "¡Advertencia: Este ingrediente vence muy pronto!"
|
||||
}
|
||||
},
|
||||
"recipes": {
|
||||
"why": "Las recetas conectan tu inventario con la producción. El sistema calculará los costos exactos por artículo, rastreará el consumo de ingredientes y te ayudará a optimizar la rentabilidad de tu menú.",
|
||||
"quick_start": "Plantillas de Recetas",
|
||||
"quick_start_desc": "Comienza con recetas probadas y personalízalas según tus necesidades",
|
||||
"category": {
|
||||
"breads": "Panes",
|
||||
"pastries": "Bollería",
|
||||
"cakes": "Pasteles y Tartas",
|
||||
"cookies": "Galletas"
|
||||
},
|
||||
"use_template": "Usar Plantilla",
|
||||
"templates_hint": "Las plantillas coincidirán automáticamente con tus ingredientes. Revisa y ajusta según sea necesario.",
|
||||
"show_templates": "Mostrar Plantillas de Recetas",
|
||||
"prerequisites_title": "Se necesitan más ingredientes",
|
||||
"prerequisites_desc": "Necesitas al menos 2 ingredientes en tu inventario antes de crear recetas. Regresa al paso de Inventario para agregar más ingredientes.",
|
||||
"added_count": "{{count}} receta agregada",
|
||||
"added_count_plural": "{{count}} recetas agregadas",
|
||||
"minimum_met": "{{count}} receta(s) agregada(s) - ¡Listo para continuar!",
|
||||
"your_recipes": "Tus Recetas",
|
||||
"yield_label": "Rendimiento",
|
||||
"add_recipe": "Agregar Receta",
|
||||
"add_first": "Agrega tu Primera Receta",
|
||||
"add_another": "Agregar Otra Receta",
|
||||
"add_new_ingredient": "Agregar Nuevo Ingrediente",
|
||||
"select_ingredient": "Seleccionar...",
|
||||
"add_ingredient": "Agregar Ingrediente",
|
||||
"no_ingredients": "Aún no se han agregado ingredientes",
|
||||
"confirm_delete": "¿Estás seguro de que deseas eliminar esta receta?",
|
||||
"fields": {
|
||||
"name": "Nombre de la Receta",
|
||||
"finished_product": "Producto Terminado",
|
||||
"yield_quantity": "Cantidad de Rendimiento",
|
||||
"yield_unit": "Unidad",
|
||||
"ingredients": "Ingredientes"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "ej., Baguette, Croissant",
|
||||
"finished_product": "Seleccionar producto terminado..."
|
||||
},
|
||||
"errors": {
|
||||
"name_required": "El nombre de la receta es obligatorio",
|
||||
"finished_product_required": "El producto terminado es obligatorio",
|
||||
"yield_invalid": "El rendimiento debe ser un número positivo",
|
||||
"ingredients_required": "Se requiere al menos un ingrediente",
|
||||
"ingredient_required": "Se requiere un ingrediente",
|
||||
"quantity_invalid": "La cantidad debe ser positiva"
|
||||
}
|
||||
},
|
||||
"quality": {
|
||||
"why": "Los controles de calidad aseguran una producción consistente y te ayudan a identificar problemas temprano. Define qué significa \"bueno\" para cada etapa de producción.",
|
||||
"optional_note": "Puedes omitir esto y configurar los controles de calidad más tarde",
|
||||
"added_count": "{{count}} control de calidad agregado",
|
||||
"added_count_plural": "{{count}} controles de calidad agregados",
|
||||
"recommended_met": "Cantidad recomendada cumplida",
|
||||
"recommended": "2+ recomendados (opcional)",
|
||||
"your_checks": "Tus Controles de Calidad",
|
||||
"add_check": "Agregar Control de Calidad",
|
||||
"add_first": "Agrega tu Primer Control de Calidad",
|
||||
"add_another": "Agregar Otro Control de Calidad",
|
||||
"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)"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "ej., Control de color de corteza, Temperatura de masa",
|
||||
"description": "Qué debe verificarse y por qué..."
|
||||
},
|
||||
"errors": {
|
||||
"name_required": "El nombre es obligatorio",
|
||||
"stages_required": "Se requiere al menos una etapa"
|
||||
}
|
||||
},
|
||||
"team": {
|
||||
"why": "Agregar miembros del equipo te permite asignar tareas, rastrear quién hace qué y dar a todos las herramientas que necesitan para trabajar eficientemente.",
|
||||
"optional_note": "Puedes agregar miembros del equipo ahora o invitarlos más tarde desde la configuración",
|
||||
"invitation_note": "Los miembros del equipo recibirán correos de invitación una vez que completes el asistente de configuración.",
|
||||
"added_count": "{{count}} miembro del equipo agregado",
|
||||
"added_count_plural": "{{count}} miembros del equipo agregados",
|
||||
"your_team": "Los Miembros de tu Equipo",
|
||||
"add_member": "Agregar Miembro del Equipo",
|
||||
"add_first": "Agrega tu Primer Miembro del Equipo",
|
||||
"add_another": "Agregar Otro Miembro del Equipo",
|
||||
"skip_message": "¿Trabajas solo por ahora? ¡No hay problema!",
|
||||
"skip_hint": "Siempre puedes invitar miembros del equipo más tarde desde Configuración → Equipo",
|
||||
"fields": {
|
||||
"name": "Nombre Completo",
|
||||
"email": "Dirección de Correo",
|
||||
"role": "Rol"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "ej., María García",
|
||||
"email": "ej., maria@panaderia.com"
|
||||
},
|
||||
"errors": {
|
||||
"name_required": "El nombre es obligatorio",
|
||||
"email_required": "El correo es obligatorio",
|
||||
"email_invalid": "Formato de correo inválido",
|
||||
"email_duplicate": "Este correo ya ha sido agregado"
|
||||
}
|
||||
},
|
||||
"review": {
|
||||
"title": "Revisa tu Configuración",
|
||||
"subtitle": "Revisemos todo lo que has configurado. Puedes regresar y hacer cambios si es necesario.",
|
||||
"suppliers": "Proveedores",
|
||||
"ingredients": "Ingredientes",
|
||||
"recipes": "Recetas",
|
||||
"quality": "Controles de Calidad",
|
||||
"suppliers_title": "Proveedores",
|
||||
"more": "más",
|
||||
"ingredients_title": "Artículos de Inventario",
|
||||
"total_cost": "Valor total",
|
||||
"recipes_title": "Recetas",
|
||||
"avg_ingredients": "Prom. ingredientes",
|
||||
"yields": "Rendimiento",
|
||||
"cost": "Costo",
|
||||
"quality_title": "Plantillas de Control de Calidad",
|
||||
"required": "Obligatorio",
|
||||
"ready_title": "¡Tu Panadería está Lista!",
|
||||
"ready_message": "Has configurado exitosamente {{suppliers}} proveedores, {{ingredients}} ingredientes y {{recipes}} recetas. Haz clic en 'Completar Configuración' para finalizar y comenzar a usar el sistema.",
|
||||
"help": "¿Necesitas hacer cambios? Usa el botón \"Atrás\" para volver a cualquier paso."
|
||||
},
|
||||
"completion": {
|
||||
"title": "🎉 ¡Configuración Completa!",
|
||||
"subtitle": "¡Felicitaciones! Tu sistema de gestión de panadería está listo para usar. Comencemos con tus primeras tareas.",
|
||||
"next_steps": "Próximos Pasos Recomendados",
|
||||
"step1_title": "Iniciar Producción",
|
||||
"step1_desc": "Crea tu primer lote de producción usando tus recetas configuradas",
|
||||
"step1_action": "Ir a Producción",
|
||||
"step2_title": "Ordenar Inventario",
|
||||
"step2_desc": "Realiza tu primera orden de compra con tus proveedores",
|
||||
"step2_action": "Ver Adquisiciones",
|
||||
"step3_title": "Seguir Analíticas",
|
||||
"step3_desc": "Monitorea tu eficiencia de producción y costos en tiempo real",
|
||||
"step3_action": "Ver Analíticas",
|
||||
"tips": "Consejos Pro para el Éxito",
|
||||
"tip1_title": "Mantén el Inventario Actualizado",
|
||||
"tip1_desc": "Actualiza regularmente los niveles de stock para obtener cálculos de costos precisos y alertas de stock bajo",
|
||||
"tip2_title": "Monitorea las Métricas de Calidad",
|
||||
"tip2_desc": "Usa controles de calidad durante la producción para identificar problemas temprano y mantener la consistencia",
|
||||
"tip3_title": "Revisa las Analíticas Semanalmente",
|
||||
"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",
|
||||
"dashboard_desc": "Ver resumen",
|
||||
"recipes": "Recetas",
|
||||
"recipes_desc": "Gestionar recetas",
|
||||
"go_dashboard": "Ir al Panel",
|
||||
"thanks": "¡Gracias por completar la configuración! ¡Feliz horneado! 🥖🥐🍰"
|
||||
}
|
||||
}
|
||||
@@ -122,5 +122,29 @@
|
||||
"last_7_days": "Azken 7 egun",
|
||||
"last_30_days": "Azken 30 egun",
|
||||
"last_90_days": "Azken 90 egun"
|
||||
},
|
||||
"config": {
|
||||
"title": "Osatu Zure Okindegiaren Konfigurazioa",
|
||||
"subtitle": "Konfiguratu ezinbesteko eginbideak hasteko",
|
||||
"inventory": "Inbentarioa",
|
||||
"suppliers": "Hornitzaileak",
|
||||
"recipes": "Errezetak",
|
||||
"quality": "Kalitate Estandarrak",
|
||||
"add_ingredients": "Gehitu gutxienez {{count}} osagai",
|
||||
"add_supplier": "Gehitu zure lehen hornitzailea",
|
||||
"add_recipe": "Sortu zure lehen errezeta",
|
||||
"add_quality": "Gehitu kalitate kontrolak (aukerakoa)",
|
||||
"sections_complete": "atal osatuta",
|
||||
"added": "gehituta",
|
||||
"recommended": "gomendatua",
|
||||
"next_step": "Hurrengo Urratsa",
|
||||
"configure": "Konfiguratu",
|
||||
"features_unlocked": "Eginbideak Desblokeatuta!",
|
||||
"features": {
|
||||
"inventory_tracking": "Inbentario Jarraipena",
|
||||
"purchase_orders": "Erosketa Aginduak",
|
||||
"production_planning": "Ekoizpen Plangintza",
|
||||
"cost_analysis": "Kostu Analisia"
|
||||
}
|
||||
}
|
||||
}
|
||||
276
frontend/src/locales/eu/setup_wizard.json
Normal file
276
frontend/src/locales/eu/setup_wizard.json
Normal file
@@ -0,0 +1,276 @@
|
||||
{
|
||||
"why_this_matters": "Zergatik da garrantzitsua",
|
||||
"optional": "Aukerakoa",
|
||||
"navigation": {
|
||||
"continue": "Jarraitu →",
|
||||
"back": "← Atzera",
|
||||
"skip": "Orain saltatu"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Bikain! Zure IA prest dago",
|
||||
"subtitle": "Orain zure okindegiko eguneroko eragiketak konfiguratu ditzagun sistemak kudeatzeko lagundu diezazun:",
|
||||
"feature_inventory": "Inbentario Jarraipena",
|
||||
"feature_inventory_desc": "Denbora errealeko stock mailak eta birpornitzeko alertak",
|
||||
"feature_recipes": "Errezeta Kostuak",
|
||||
"feature_recipes_desc": "Kostu kalkulua automatikoa eta errentagarritasun analisia",
|
||||
"feature_quality": "Kalitate Monitorizazioa",
|
||||
"feature_quality_desc": "Estandarren eta ekoizpen kalitatearen jarraipena",
|
||||
"feature_team": "Talde Koordinazioa",
|
||||
"feature_team_desc": "Zereginak esleitu eta erantzukizunen jarraipena",
|
||||
"time_estimate": "Gutxi gorabehera 15-20 minutu behar dira",
|
||||
"save_resume": "Aurrerapena gorde eta edozein unetan berrekin dezakezu",
|
||||
"skip": "Geroago egingo dut",
|
||||
"get_started": "Has gaitezen! →"
|
||||
},
|
||||
"suppliers": {
|
||||
"why": "Hornitzaileak zure osagaien iturria dira. Orain konfiguratuz, kostuak jarraitu, eskaerak kudeatu eta hornitzaileen errendimendua aztertu dezakezu.",
|
||||
"added_count": "Hornitzaile {{count}} gehituta",
|
||||
"added_count_plural": "{{count}} hornitzaile gehituta",
|
||||
"minimum_met": "Gutxieneko baldintza betetzen da",
|
||||
"add_minimum": "Gehitu gutxienez hornitzaile 1 jarraitzeko",
|
||||
"your_suppliers": "Zure Hornitzaileak",
|
||||
"confirm_delete": "Ziur zaude hornitzaile hau ezabatu nahi duzula?",
|
||||
"edit_supplier": "Hornitzailea Editatu",
|
||||
"add_supplier": "Hornitzailea Gehitu",
|
||||
"add_first": "Gehitu Zure Lehen Hornitzailea",
|
||||
"add_another": "Beste Hornitzaile Bat Gehitu",
|
||||
"manage_products": "Produktuak Kudeatu",
|
||||
"products": "produktuak",
|
||||
"products_for": "{{name}}-(r)entzako produktuak",
|
||||
"add_products": "Produktuak Gehitu",
|
||||
"no_products_available": "Ez dago produkturik eskuragarri",
|
||||
"select_products": "Produktuak Aukeratu",
|
||||
"unit_price": "Prezioa",
|
||||
"unit": "Unitatea",
|
||||
"min_qty": "Kant. Gutx.",
|
||||
"add_new_product": "Produktu Berria Gehitu",
|
||||
"save_products": "Gorde",
|
||||
"no_products_warning": "Gehitu gutxienez produktu 1 erosketa-agindu automatikoak gaitzeko",
|
||||
"fields": {
|
||||
"name": "Hornitzailearen Izena",
|
||||
"type": "Mota",
|
||||
"contact_person": "Kontaktu Pertsona",
|
||||
"phone": "Telefonoa",
|
||||
"email": "Posta Elektronikoa"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "adib., Molinos SA, Distribuidora López",
|
||||
"contact_person": "adib., Juan Pérez",
|
||||
"phone": "adib., +34 91 123 4567",
|
||||
"email": "adib., salmentak@hornitzailea.eus"
|
||||
},
|
||||
"errors": {
|
||||
"name_required": "Izena beharrezkoa da",
|
||||
"email_invalid": "Posta formatu baliogabea"
|
||||
}
|
||||
},
|
||||
"inventory": {
|
||||
"why": "Inbentario osagaiak zure errezeten oinarrizko elementuak dira. Konfiguratuta, sistemak kantitateen jarraipena egingo du, stock apala dagoenean alertak bidaliko ditu eta errezeten kostuak kalkulatzen lagunduko dizu.",
|
||||
"quick_start": "Abio Azkarra",
|
||||
"quick_start_desc": "Inportatu ohiko osagaiak azkar hasteko",
|
||||
"essential": "Oinarrizko Osagaiak",
|
||||
"common": "Ohiko Osagaiak",
|
||||
"packaging": "Ontziratzea",
|
||||
"import_all": "Dena Inportatu",
|
||||
"templates_hint": "Klik egin edozein elementutan gehitu aurretik pertsonalizatzeko, edo erabili \"Dena Inportatu\" konfigurazio azkarrerako",
|
||||
"show_templates": "Erakutsi Abio Azkarreko Txantiloiak",
|
||||
"added_count": "Osagai {{count}} gehituta",
|
||||
"added_count_plural": "{{count}} osagai gehituta",
|
||||
"minimum_met": "Gutxieneko baldintza betetzen da",
|
||||
"need_more": "{{count}} gehiago behar dira",
|
||||
"your_ingredients": "Zure Osagaiak",
|
||||
"add_ingredient": "Osagaia Gehitu",
|
||||
"edit_ingredient": "Osagaia Editatu",
|
||||
"add_first": "Gehitu Zure Lehen Osagaia",
|
||||
"add_another": "Beste Osagai Bat Gehitu",
|
||||
"confirm_delete": "Ziur zaude osagai hau ezabatu nahi duzula?",
|
||||
"add_stock": "Stock Hasiera Gehitu",
|
||||
"quantity": "Kantitatea",
|
||||
"expiration_date": "Iraungitze Data",
|
||||
"supplier": "Hornitzailea",
|
||||
"batch_number": "Lote Zenbakia",
|
||||
"stock_help": "Iraungitze jarraipenak hondakinak prebenitzen laguntzen du eta FIFO inbentario kudeaketa gaitzen du",
|
||||
"add_another_lot": "+ Beste Lote Bat Gehitu",
|
||||
"add_another_stock": "Beste Stock Lote Bat Gehitu",
|
||||
"add_initial_stock": "Stock Hasiera Gehitu (Aukerakoa)",
|
||||
"fields": {
|
||||
"name": "Osagaiaren Izena",
|
||||
"category": "Kategoria",
|
||||
"unit": "Neurri Unitatea",
|
||||
"brand": "Marka",
|
||||
"cost": "Kostu Estandarra"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "adib., Irina 000, Legami freskoa",
|
||||
"brand": "adib., Molinos Río",
|
||||
"cost": "adib., 150.00"
|
||||
},
|
||||
"errors": {
|
||||
"name_required": "Izena beharrezkoa da",
|
||||
"cost_invalid": "Kostua zenbaki baliozkoa izan behar da",
|
||||
"threshold_invalid": "Atalasea zenbaki baliozkoa izan behar da"
|
||||
},
|
||||
"stock_errors": {
|
||||
"quantity_required": "Kantitatea zero baino handiagoa izan behar da",
|
||||
"expiration_past": "Iraungitze data iraganean dago",
|
||||
"expiring_soon": "Abisua: Osagai hau laster iraungitzen da!"
|
||||
}
|
||||
},
|
||||
"recipes": {
|
||||
"why": "Errezetak zure inbentarioa ekoizpenarekin konektatzen dute. Sistemak elementu bakoitzeko kostu zehatzak kalkulatuko ditu, osagaien kontsumoa jarraituko du eta menuko errentagarritasuna optimizatzen lagunduko dizu.",
|
||||
"quick_start": "Errezeta Txantiloiak",
|
||||
"quick_start_desc": "Hasi frogatutako errezetekin eta pertsonalizatu zure beharretara",
|
||||
"category": {
|
||||
"breads": "Ogiak",
|
||||
"pastries": "Gozogintza",
|
||||
"cakes": "Pastelak eta Tartak",
|
||||
"cookies": "Galletak"
|
||||
},
|
||||
"use_template": "Txantiloia Erabili",
|
||||
"templates_hint": "Txantiloiek automatikoki zure osagaiekin bat egingo dute. Berrikusi eta egokitu behar den bezala.",
|
||||
"show_templates": "Erakutsi Errezeta Txantiloiak",
|
||||
"prerequisites_title": "Osagai gehiago behar dira",
|
||||
"prerequisites_desc": "Gutxienez 2 osagai behar dituzu zure inbentarioan errezetak sortu aurretik. Itzuli Inbentario urratsera osagai gehiago gehitzeko.",
|
||||
"added_count": "Errezeta {{count}} gehituta",
|
||||
"added_count_plural": "{{count}} errezeta gehituta",
|
||||
"minimum_met": "{{count}} errezeta gehituta - Jarraitzeko prest!",
|
||||
"your_recipes": "Zure Errezetak",
|
||||
"yield_label": "Etekin",
|
||||
"add_recipe": "Errezeta Gehitu",
|
||||
"add_first": "Gehitu Zure Lehen Errezeta",
|
||||
"add_another": "Beste Errezeta Bat Gehitu",
|
||||
"add_new_ingredient": "Osagai Berria Gehitu",
|
||||
"select_ingredient": "Aukeratu...",
|
||||
"add_ingredient": "Osagaia Gehitu",
|
||||
"no_ingredients": "Oraindik ez da osagairik gehitu",
|
||||
"confirm_delete": "Ziur zaude errezeta hau ezabatu nahi duzula?",
|
||||
"fields": {
|
||||
"name": "Errezeta Izena",
|
||||
"finished_product": "Produktu Amaituak",
|
||||
"yield_quantity": "Etekinaren Kantitatea",
|
||||
"yield_unit": "Unitatea",
|
||||
"ingredients": "Osagaiak"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "adib., Baguette, Croissant",
|
||||
"finished_product": "Aukeratu produktu amaituak..."
|
||||
},
|
||||
"errors": {
|
||||
"name_required": "Errezeta izena beharrezkoa da",
|
||||
"finished_product_required": "Produktu amaituak beharrezkoa da",
|
||||
"yield_invalid": "Etekina zenbaki positiboa izan behar da",
|
||||
"ingredients_required": "Gutxienez osagai bat beharrezkoa da",
|
||||
"ingredient_required": "Osagaia beharrezkoa da",
|
||||
"quantity_invalid": "Kantitatea positiboa izan behar da"
|
||||
}
|
||||
},
|
||||
"quality": {
|
||||
"why": "Kalitate kontrolek irteera koherentea bermatzen dute eta goiz arazoak identifikatzen laguntzen dizute. Definitu zer den \"ona\" ekoizpen etapa bakoitzerako.",
|
||||
"optional_note": "Hau saltatu eta kalitate kontrolak geroago konfigura ditzakezu",
|
||||
"added_count": "Kalitate kontrol {{count}} gehituta",
|
||||
"added_count_plural": "{{count}} kalitate kontrol gehituta",
|
||||
"recommended_met": "Gomendatutako kopurua betetzen da",
|
||||
"recommended": "2+ gomendatzen dira (aukerakoa)",
|
||||
"your_checks": "Zure Kalitate Kontrolak",
|
||||
"add_check": "Kalitate Kontrola Gehitu",
|
||||
"add_first": "Gehitu Zure Lehen Kalitate Kontrola",
|
||||
"add_another": "Beste Kalitate Kontrol Bat Gehitu",
|
||||
"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)"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "adib., Azal kolorearen kontrola, Oraren tenperatura",
|
||||
"description": "Zer egiaztatu behar den eta zergatik..."
|
||||
},
|
||||
"errors": {
|
||||
"name_required": "Izena beharrezkoa da",
|
||||
"stages_required": "Gutxienez etapa bat beharrezkoa da"
|
||||
}
|
||||
},
|
||||
"team": {
|
||||
"why": "Taldekideak gehitzeak zereginak esleitzea, nork zer egiten duen jarraitzea eta guztiei behar dituzten tresnak ematea ahalbidetzen dizu modu eraginkorrean lan egiteko.",
|
||||
"optional_note": "Taldekideak orain gehi ditzakezu edo ezarpenetatik geroago gonbida ditzakezu",
|
||||
"invitation_note": "Taldekideek gonbidapen posta elektronikoak jasoko dituzte konfigurazio morroia osatu ondoren.",
|
||||
"added_count": "Taldekide {{count}} gehituta",
|
||||
"added_count_plural": "{{count}} taldekide gehituta",
|
||||
"your_team": "Zure Taldekideak",
|
||||
"add_member": "Taldekidea Gehitu",
|
||||
"add_first": "Gehitu Zure Lehen Taldekidea",
|
||||
"add_another": "Beste Taldekide Bat Gehitu",
|
||||
"skip_message": "Oraingoz bakarrik lanean? Ez dago arazorik!",
|
||||
"skip_hint": "Beti gehi ditzakezu taldekideak geroago Ezarpenak → Taldea-tik",
|
||||
"fields": {
|
||||
"name": "Izen Osoa",
|
||||
"email": "Posta Elektroniko Helbidea",
|
||||
"role": "Rola"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "adib., María García",
|
||||
"email": "adib., maria@okindegi.eus"
|
||||
},
|
||||
"errors": {
|
||||
"name_required": "Izena beharrezkoa da",
|
||||
"email_required": "Posta beharrezkoa da",
|
||||
"email_invalid": "Posta formatu baliogabea",
|
||||
"email_duplicate": "Posta elektroniko hau dagoeneko gehituta dago"
|
||||
}
|
||||
},
|
||||
"review": {
|
||||
"title": "Berrikusi Zure Konfigurazioa",
|
||||
"subtitle": "Berrikusi ditzagun konfiguratu dituzun guztiak. Atzera joan eta aldaketak egin ditzakezu behar izanez gero.",
|
||||
"suppliers": "Hornitzaileak",
|
||||
"ingredients": "Osagaiak",
|
||||
"recipes": "Errezetak",
|
||||
"quality": "Kalitate Kontrolak",
|
||||
"suppliers_title": "Hornitzaileak",
|
||||
"more": "gehiago",
|
||||
"ingredients_title": "Inbentario Elementuak",
|
||||
"total_cost": "Balio osoa",
|
||||
"recipes_title": "Errezetak",
|
||||
"avg_ingredients": "Batez besteko osagaiak",
|
||||
"yields": "Etekina",
|
||||
"cost": "Kostua",
|
||||
"quality_title": "Kalitate Kontrol Txantiloiak",
|
||||
"required": "Nahitaezkoa",
|
||||
"ready_title": "Zure Okindegi Prest Dago!",
|
||||
"ready_message": "Arrakastaz konfiguratu dituzu {{suppliers}} hornitzaile, {{ingredients}} osagai eta {{recipes}} errezeta. Egin klik 'Konfigurazioa Osatu'-n amaitzeko eta sistema erabiltzen hasteko.",
|
||||
"help": "Aldaketak egin behar dituzu? Erabili \"Atzera\" botoia edozein urratsera itzultzeko."
|
||||
},
|
||||
"completion": {
|
||||
"title": "🎉 Konfigurazioa Osatuta!",
|
||||
"subtitle": "Zorionak! Zure okindegi kudeaketa sistema erabiltzeko prest dago. Has gaitezen zure lehen zereginekin.",
|
||||
"next_steps": "Gomendatutako Hurrengo Urratsak",
|
||||
"step1_title": "Ekoizpena Hasi",
|
||||
"step1_desc": "Sortu zure lehen ekoizpen lotea konfiguratutako errezetek erabiliz",
|
||||
"step1_action": "Joan Ekoizpenera",
|
||||
"step2_title": "Inbentarioa Eskatu",
|
||||
"step2_desc": "Egin zure lehen erosketa-agindua zure hornitzaileekin",
|
||||
"step2_action": "Ikusi Erosketak",
|
||||
"step3_title": "Jarraitu Analitikak",
|
||||
"step3_desc": "Zaindu zure ekoizpen eraginkortasuna eta kostuak denbora errealean",
|
||||
"step3_action": "Ikusi Analitikak",
|
||||
"tips": "Arrakastako Aholku Profesionalak",
|
||||
"tip1_title": "Mantendu Inbentarioa Eguneratuta",
|
||||
"tip1_desc": "Eguneratu stock mailak erregularki kostu kalkulu zehatzak eta stock apala alertak lortzeko",
|
||||
"tip2_title": "Zaindu Kalitate Metrikak",
|
||||
"tip2_desc": "Erabili kalitate kontrolak ekoizpenean goiz arazoak identifikatzeko eta koherentzia mantentzeko",
|
||||
"tip3_title": "Berrikusi Analitikak Astero",
|
||||
"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",
|
||||
"dashboard_desc": "Ikusi laburpena",
|
||||
"recipes": "Errezetak",
|
||||
"recipes_desc": "Kudeatu errezetak",
|
||||
"go_dashboard": "Joan Aginte-panelera",
|
||||
"thanks": "Eskerrik asko konfigurazioa osatzeagatik! Labealdi zoriontsuak! 🥖🥐🍰"
|
||||
}
|
||||
}
|
||||
@@ -48,19 +48,17 @@ ONBOARDING_STEPS = [
|
||||
|
||||
# Phase 2: Core Setup
|
||||
"setup", # Basic bakery setup and tenant creation
|
||||
# NOTE: POI detection now happens automatically in background during tenant registration
|
||||
|
||||
# Phase 2a: POI Detection (Location Context)
|
||||
"poi-detection", # Detect nearby POIs for location-based ML features
|
||||
|
||||
# Phase 2b: AI-Assisted Inventory Setup (REFACTORED - split into 3 focused steps)
|
||||
# Phase 2a: AI-Assisted Inventory Setup (REFACTORED - split into 3 focused steps)
|
||||
"upload-sales-data", # File upload, validation, and AI classification
|
||||
"inventory-review", # Review and confirm AI-detected products with type selection
|
||||
"initial-stock-entry", # Capture initial stock levels
|
||||
|
||||
# Phase 2c: Product Categorization (optional advanced categorization)
|
||||
# Phase 2b: Product Categorization (optional advanced categorization)
|
||||
"product-categorization", # Advanced categorization (may be deprecated)
|
||||
|
||||
# Phase 2d: Suppliers (shared by all paths)
|
||||
# Phase 2c: Suppliers (shared by all paths)
|
||||
"suppliers-setup", # Suppliers configuration
|
||||
|
||||
# Phase 3: Advanced Configuration (all optional)
|
||||
@@ -71,7 +69,7 @@ ONBOARDING_STEPS = [
|
||||
|
||||
# Phase 4: ML & Finalization
|
||||
"ml-training", # AI model training
|
||||
"setup-review", # Review all configuration
|
||||
# "setup-review" removed - not useful for user, completion step is final
|
||||
"completion" # Onboarding completed
|
||||
]
|
||||
|
||||
@@ -83,9 +81,7 @@ STEP_DEPENDENCIES = {
|
||||
|
||||
# Core setup - no longer depends on data-source-choice (removed)
|
||||
"setup": ["user_registered", "bakery-type-selection"],
|
||||
|
||||
# POI Detection - requires tenant creation (setup)
|
||||
"poi-detection": ["user_registered", "setup"],
|
||||
# NOTE: POI detection removed from steps - now happens automatically in background
|
||||
|
||||
# AI-Assisted Inventory Setup - REFACTORED into 3 sequential steps
|
||||
"upload-sales-data": ["user_registered", "setup"],
|
||||
|
||||
Reference in New Issue
Block a user