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:
ualsweb
2025-11-12 16:19:33 +01:00
committed by GitHub
19 changed files with 1373 additions and 202 deletions

View File

@@ -11,7 +11,6 @@ import { WizardProvider, useWizardContext, BakeryType, DataSource } from './cont
import { import {
BakeryTypeSelectionStep, BakeryTypeSelectionStep,
RegisterTenantStep, RegisterTenantStep,
POIDetectionStep,
FileUploadStep, FileUploadStep,
InventoryReviewStep, InventoryReviewStep,
ProductCategorizationStep, ProductCategorizationStep,
@@ -75,15 +74,7 @@ const OnboardingWizardContent: React.FC = () => {
isConditional: true, isConditional: true,
condition: (ctx) => ctx.state.bakeryType !== null, condition: (ctx) => ctx.state.bakeryType !== null,
}, },
// Phase 2b: POI Detection // POI Detection removed - now happens automatically in background after tenant registration
{
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,
},
// Phase 2a: AI-Assisted Inventory Setup (REFACTORED - split into 3 focused steps) // Phase 2a: AI-Assisted Inventory Setup (REFACTORED - split into 3 focused steps)
{ {
id: 'upload-sales-data', id: 'upload-sales-data',
@@ -159,14 +150,7 @@ const OnboardingWizardContent: React.FC = () => {
component: MLTrainingStep, component: MLTrainingStep,
// Always show - no conditional // Always show - no conditional
}, },
{ // Revision step removed - not useful for user, completion step is final step
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
},
{ {
id: 'completion', id: 'completion',
title: t('onboarding:steps.completion.title', 'Completado'), 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 // Show loading state
if (!isNewTenant && (isLoadingProgress || !isInitialized)) { if (!isNewTenant && (isLoadingProgress || !isInitialized)) {
return ( return (
@@ -534,7 +528,7 @@ const OnboardingWizardContent: React.FC = () => {
<CardBody padding="md"> <CardBody padding="md">
<StepComponent <StepComponent
onNext={() => {}} onNext={() => {}}
onPrevious={() => {}} onPrevious={handleGoToPrevious}
onComplete={handleStepComplete} onComplete={handleStepComplete}
onUpdate={handleStepUpdate} onUpdate={handleStepUpdate}
isFirstStep={currentStepIndex === 0} isFirstStep={currentStepIndex === 0}
@@ -562,12 +556,6 @@ const OnboardingWizardContent: React.FC = () => {
initialStock: undefined, initialStock: undefined,
})) }))
} }
: // Pass tenant info to POI detection step
currentStep.id === 'poi-detection'
? {
tenantId: wizardContext.state.tenantId,
bakeryLocation: wizardContext.state.bakeryLocation,
}
: undefined : undefined
} }
/> />

View File

@@ -269,7 +269,7 @@ export const WizardProvider: React.FC<WizardProviderProps> = ({
steps.push('ml-training'); steps.push('ml-training');
} }
steps.push('setup-review'); // Revision step removed - not useful for user
steps.push('completion'); steps.push('completion');
return steps; return steps;

View File

@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button } from '../../../ui/Button'; import { Button } from '../../../ui/Button';
import { useCurrentTenant } from '../../../../stores/tenant.store'; import { useCurrentTenant } from '../../../../stores/tenant.store';
import { ChartBar, ShoppingCart, Users, TrendingUp, Zap, CheckCircle2 } from 'lucide-react';
interface CompletionStepProps { interface CompletionStepProps {
onNext: () => void; onNext: () => void;
@@ -30,20 +31,21 @@ export const CompletionStep: React.FC<CompletionStepProps> = ({
}; };
return ( return (
<div className="text-center space-y-8"> <div className="text-center space-y-8 max-w-5xl mx-auto">
{/* Success Icon */} {/* Animated Success Icon */}
<div className="mx-auto w-24 h-24 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center"> <div className="relative mx-auto w-32 h-32">
<svg className="w-12 h-12 text-[var(--color-success)]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <div className="absolute inset-0 bg-[var(--color-success)]/20 rounded-full animate-ping"></div>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> <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">
</svg> <CheckCircle2 className="w-16 h-16 text-white" />
</div>
</div> </div>
{/* Success Message */} {/* Success Message */}
<div className="space-y-4"> <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')} {t('onboarding:completion.congratulations', '¡Felicidades! Tu Sistema Está Listo')}
</h1> </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 })} {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> </p>
</div> </div>
@@ -140,49 +142,109 @@ export const CompletionStep: React.FC<CompletionStepProps> = ({
</div> </div>
</div> </div>
{/* Next Steps */} {/* Quick Access Cards */}
<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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 max-w-4xl mx-auto">
<div className="flex items-start gap-4"> <button
<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"> 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"
</div> >
<div> <ChartBar className="w-8 h-8 text-[var(--color-primary)] mb-2 group-hover:scale-110 transition-transform" />
<h3 className="font-semibold text-lg mb-2 text-[var(--text-primary)]">{t('onboarding:completion.ready_to_start', '¡Listo para Empezar!')}</h3> <h4 className="font-semibold text-[var(--text-primary)] mb-1">
<p className="text-sm text-[var(--text-secondary)] mb-3"> {t('onboarding:completion.quick.analytics', 'Analíticas')}
{t('onboarding:completion.explore_message', 'Ahora puedes explorar el panel de control y comenzar a gestionar tu panadería con inteligencia artificial.')} </h4>
<p className="text-xs text-[var(--text-secondary)]">
{t('onboarding:completion.quick.analytics_desc', 'Ver predicciones y métricas')}
</p> </p>
<div className="space-y-2"> </button>
<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"> <button
<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" /> onClick={() => navigate('/app/inventory')}
</svg> 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"
<span className="text-[var(--text-secondary)]">{t('onboarding:completion.view_analytics', 'Ve análisis y predicciones de demanda')}</span> >
<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>
<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"> {/* Tips for Success */}
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> <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">
</svg> <div className="flex items-start gap-4">
<span className="text-[var(--text-secondary)]">{t('onboarding:completion.manage_operations', 'Gestiona producción y operaciones diarias')}</span> <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>
<div className="flex items-start gap-2 text-sm"> <div className="flex-1">
<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"> <h3 className="font-bold text-lg mb-3 text-[var(--text-primary)]">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" /> {t('onboarding:completion.tips_title', 'Consejos para Maximizar tu Éxito')}
</svg> </h3>
<span className="text-[var(--text-secondary)]">{t('onboarding:completion.optimize_costs', 'Optimiza costos y reduce desperdicios')}</span> <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> </div>
</div> </div>
</div> </div>
{/* Action Buttons */} {/* Primary Action Button */}
<div className="flex justify-center items-center pt-4"> <div className="flex justify-center items-center pt-4">
<Button <Button
onClick={handleExploreDashboard} onClick={handleExploreDashboard}
size="lg" 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> </Button>
</div> </div>

View File

@@ -39,6 +39,8 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [progressState, setProgressState] = useState<ProgressState | null>(null); const [progressState, setProgressState] = useState<ProgressState | null>(null);
const [showGuide, setShowGuide] = useState(false); 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 fileInputRef = useRef<HTMLInputElement>(null);
const currentTenant = useCurrentTenant(); const currentTenant = useCurrentTenant();
@@ -101,6 +103,13 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
throw new Error(errorMsg); 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 // Step 2: Extract product list
setProgressState({ setProgressState({
stage: 'analyzing', stage: 'analyzing',
@@ -158,6 +167,8 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
setSelectedFile(null); setSelectedFile(null);
setError(''); setError('');
setProgressState(null); setProgressState(null);
setValidationSuccess(false);
setValidationDetails(null);
if (fileInputRef.current) { if (fileInputRef.current) {
fileInputRef.current.value = ''; fileInputRef.current.value = '';
} }
@@ -215,14 +226,14 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
)} )}
{/* Selected File Preview */} {/* Selected File Preview */}
{selectedFile && !isProcessing && ( {selectedFile && !isProcessing && !validationSuccess && (
<div className="border border-[var(--color-success)] bg-[var(--color-success)]/5 rounded-lg p-3 md:p-4"> <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 justify-between gap-2">
<div className="flex items-center gap-2 md:gap-3 min-w-0"> <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"> <div className="min-w-0">
<p className="font-medium text-[var(--text-primary)] text-sm md:text-base truncate">{selectedFile.name}</p> <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 {(selectedFile.size / 1024).toFixed(2)} KB
</p> </p>
</div> </div>
@@ -237,6 +248,40 @@ export const FileUploadStep: React.FC<FileUploadStepProps> = ({
</div> </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 */} {/* Progress Indicator */}
{isProcessing && progressState && ( {isProcessing && progressState && (
<div className="border border-[var(--color-primary)] rounded-lg p-6 bg-[var(--color-primary)]/5"> <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 Display */}
{error && ( {error && (
<div className="bg-[var(--color-danger)]/10 border border-[var(--color-danger)]/20 rounded-lg p-4"> <div className="bg-[var(--color-error)]/10 border-2 border-[var(--color-error)] rounded-lg p-4">
<div className="flex items-start gap-2"> <div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-[var(--color-danger)] flex-shrink-0 mt-0.5" /> <AlertCircle className="w-6 h-6 text-[var(--color-error)] flex-shrink-0 mt-0.5" />
<div> <div className="flex-1">
<p className="font-medium text-[var(--color-danger)] mb-1">Error</p> <p className="font-semibold text-[var(--color-error)] mb-2">
<p className="text-sm text-[var(--text-secondary)]">{error}</p> {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> </div>
</div> </div>

View File

@@ -4,6 +4,9 @@ import { Package, Salad, AlertCircle, ArrowRight, ArrowLeft, CheckCircle } from
import Button from '../../../ui/Button/Button'; import Button from '../../../ui/Button/Button';
import Card from '../../../ui/Card/Card'; import Card from '../../../ui/Card/Card';
import Input from '../../../ui/Input/Input'; 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 { export interface ProductWithStock {
id: string; id: string;
@@ -32,6 +35,11 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
initialData, initialData,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const addStockMutation = useAddStock();
const [isSaving, setIsSaving] = useState(false);
const [products, setProducts] = useState<ProductWithStock[]>(() => { const [products, setProducts] = useState<ProductWithStock[]>(() => {
if (initialData?.productsWithStock) { if (initialData?.productsWithStock) {
return initialData.productsWithStock; return initialData.productsWithStock;
@@ -76,8 +84,36 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
onComplete?.(); 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?.(); 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); const productsWithStock = products.filter(p => p.initialStock !== undefined && p.initialStock >= 0);
@@ -106,51 +142,30 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
} }
return ( return (
<div className="max-w-4xl mx-auto p-4 md:p-6 space-y-4 md:space-y-6"> <div className="space-y-4 md:space-y-6">
{/* Header */} {/* Why This Matters */}
<div className="text-center space-y-2 md:space-y-3"> <InfoCard
<h1 className="text-xl md:text-2xl font-bold text-text-primary px-2"> variant="info"
{t('onboarding:stock.title', 'Niveles de Stock Inicial')} title={t('setup_wizard:why_this_matters', '¿Por qué es importante?')}
</h1> description={t(
<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(
'onboarding:stock.info_text', '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.' '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 */} {/* Progress */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between text-sm"> <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')} {t('onboarding:stock.progress', 'Progreso de captura')}
</span> </span>
<span className="font-medium text-text-primary"> <span className="font-medium text-[var(--text-primary)]">
{productsWithStock.length} / {products.length} {productsWithStock.length} / {products.length}
</span> </span>
</div> </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 <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}%` }} style={{ width: `${completionPercentage}%` }}
/> />
</div> </div>
@@ -170,10 +185,10 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
{ingredients.length > 0 && ( {ingredients.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center"> <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-green-600" /> <Salad className="w-4 h-4 text-[var(--color-success)]" />
</div> </div>
<h3 className="font-semibold text-text-primary"> <h3 className="font-semibold text-[var(--text-primary)]">
{t('onboarding:stock.ingredients', 'Ingredientes')} ({ingredients.length}) {t('onboarding:stock.ingredients', 'Ingredientes')} ({ingredients.length})
</h3> </h3>
</div> </div>
@@ -182,16 +197,16 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
{ingredients.map(product => { {ingredients.map(product => {
const hasStock = product.initialStock !== undefined; const hasStock = product.initialStock !== undefined;
return ( 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="p-3">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="flex-1"> <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} {product.name}
{hasStock && <CheckCircle className="w-4 h-4 text-green-600" />} {hasStock && <CheckCircle className="w-4 h-4 text-[var(--color-success)]" />}
</div> </div>
{product.category && ( {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>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -204,7 +219,7 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
step="0.01" step="0.01"
className="w-20 sm:w-24 text-right min-h-[44px]" 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'} {product.unit || 'kg'}
</span> </span>
</div> </div>
@@ -221,10 +236,10 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
{finishedProducts.length > 0 && ( {finishedProducts.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center"> <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-blue-600" /> <Package className="w-4 h-4 text-[var(--color-info)]" />
</div> </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}) {t('onboarding:stock.finished_products', 'Productos Terminados')} ({finishedProducts.length})
</h3> </h3>
</div> </div>
@@ -233,16 +248,16 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
{finishedProducts.map(product => { {finishedProducts.map(product => {
const hasStock = product.initialStock !== undefined; const hasStock = product.initialStock !== undefined;
return ( 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="p-3">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="flex-1"> <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} {product.name}
{hasStock && <CheckCircle className="w-4 h-4 text-blue-600" />} {hasStock && <CheckCircle className="w-4 h-4 text-[var(--color-info)]" />}
</div> </div>
{product.category && ( {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>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -255,7 +270,7 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
step="1" step="1"
className="w-24 text-right" 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')} {product.unit || t('common:units', 'unidades')}
</span> </span>
</div> </div>
@@ -270,35 +285,34 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
{/* Warning for incomplete */} {/* Warning for incomplete */}
{!allCompleted && ( {!allCompleted && (
<Card className="bg-amber-50 border-amber-200"> <InfoCard
<div className="p-4 flex items-start gap-3"> variant="warning"
<AlertCircle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" /> title={t('onboarding:stock.incomplete_warning', 'Faltan {{count}} productos por completar', {
<div className="text-sm text-amber-900">
<p className="font-medium">
{t('onboarding:stock.incomplete_warning', 'Faltan {count} productos por completar', {
count: productsWithoutStock.length, count: productsWithoutStock.length,
})} })}
</p> description={t(
<p className="text-amber-700 mt-1">
{t(
'onboarding:stock.incomplete_help', 'onboarding:stock.incomplete_help',
'Puedes continuar, pero recomendamos ingresar todas las cantidades para un mejor control de inventario.' 'Puedes continuar, pero recomendamos ingresar todas las cantidades para un mejor control de inventario.'
)} )}
</p> />
</div>
</div>
</Card>
)} )}
{/* Footer Actions */} {/* 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 />}> <Button onClick={onPrevious} variant="ghost" leftIcon={<ArrowLeft />}>
{t('common:previous', 'Anterior')} {t('common:previous', 'Anterior')}
</Button> </Button>
<Button onClick={handleContinue} variant="primary" rightIcon={<ArrowRight />}> <Button
{allCompleted onClick={handleContinue}
? t('onboarding:stock.complete', 'Completar Configuración') 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')} : t('onboarding:stock.continue_anyway', 'Continuar de todos modos')}
</Button> </Button>
</div> </div>

View File

@@ -5,6 +5,7 @@ import { useRegisterBakery } from '../../../../api/hooks/tenant';
import { BakeryRegistration } from '../../../../api/types/tenant'; import { BakeryRegistration } from '../../../../api/types/tenant';
import { AddressResult } from '../../../../services/api/geocodingApi'; import { AddressResult } from '../../../../services/api/geocodingApi';
import { useWizardContext } from '../context'; import { useWizardContext } from '../context';
import { poiContextApi } from '../../../../services/api/poiContextApi';
interface RegisterTenantStepProps { interface RegisterTenantStepProps {
onNext: () => void; onNext: () => void;
@@ -112,8 +113,25 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
try { try {
const tenant = await registerBakery.mutateAsync(formData); const tenant = await registerBakery.mutateAsync(formData);
// Update the wizard context with tenant info and pass the bakeryLocation coordinates // Trigger POI detection in the background (non-blocking)
// that were captured during address selection to the next step (POI Detection) // 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({ onComplete({
tenant, tenant,
tenantId: tenant.id, tenantId: tenant.id,

View File

@@ -274,10 +274,10 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
console.log('Check type clicked:', option.value, 'current:', formData.check_type); console.log('Check type clicked:', option.value, 'current:', formData.check_type);
setFormData(prev => ({ ...prev, check_type: option.value })); 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 formData.check_type === option.value
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/10' ? '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(--border-primary)]' : 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
}`} }`}
> >
<div className="text-lg mb-1">{option.icon}</div> <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] : [...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) formData.applicable_stages.includes(option.value)
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/10 text-[var(--color-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(--border-primary)]' : 'border-[var(--border-secondary)] text-[var(--text-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
}`} }`}
> >
{option.label} {option.label}

View File

@@ -565,7 +565,9 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
className={`w-full px-3 py-2 bg-[var(--bg-primary)] border ${errors.finished_product_id ? 'border-[var(--color-error)]' : 'border-[var(--border-secondary)]'} rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]`} 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> <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}> <option key={ing.id} value={ing.id}>
{ing.name} ({ing.unit_of_measure}) {ing.name} ({ing.unit_of_measure})
</option> </option>
@@ -793,19 +795,6 @@ export const RecipesSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
tenantId={tenantId} tenantId={tenantId}
context="recipe" 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> </div>
); );
}; };

View File

@@ -247,10 +247,10 @@ export const TeamSetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplete,
key={option.value} key={option.value}
type="button" type="button"
onClick={() => setFormData({ ...formData, role: option.value })} 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 formData.role === option.value
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/10' ? '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(--border-primary)]' : 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
}`} }`}
> >
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">

View File

@@ -99,7 +99,7 @@ export const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
return ( return (
<div ref={wrapperRef} className={`relative ${className}`}> <div ref={wrapperRef} className={`relative ${className}`}>
<div className="relative"> <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 <Input
type="text" type="text"
@@ -109,15 +109,15 @@ export const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
placeholder={placeholder} placeholder={placeholder}
disabled={disabled} disabled={disabled}
required={required} 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"> <div className="absolute right-3 top-1/2 transform -translate-y-1/2 flex items-center gap-1">
{isLoading && ( {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 && ( {selectedAddress && !isLoading && (
<Check className="h-4 w-4 text-green-600" /> <Check className="h-4 w-4 text-[var(--color-success)]" />
)} )}
{query && !disabled && ( {query && !disabled && (
<Button <Button
@@ -125,7 +125,7 @@ export const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={handleClear} 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" /> <X className="h-3 w-3" />
</Button> </Button>
@@ -135,36 +135,36 @@ export const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
{/* Error message */} {/* Error message */}
{error && ( {error && (
<div className="mt-1 text-sm text-red-600"> <div className="mt-1 text-sm text-[var(--color-error)]">
{error} {error}
</div> </div>
)} )}
{/* Results dropdown */} {/* Results dropdown */}
{showResults && results.length > 0 && ( {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"> <CardBody className="p-0">
<div className="divide-y divide-gray-100"> <div className="divide-y divide-[var(--border-secondary)]">
{results.map((result) => ( {results.map((result) => (
<button <button
key={result.place_id} key={result.place_id}
type="button" type="button"
onClick={() => handleSelectAddress(result)} 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"> <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="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.address.house_number}` ? `${result.address.road}, ${result.address.house_number}`
: result.address.road || result.display_name} : result.address.road || result.display_name}
</div> </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.city || result.address.municipality || result.address.suburb}
{result.address.postcode && `, ${result.address.postcode}`} {result.address.postcode && `, ${result.address.postcode}`}
</div> </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)} {result.lat.toFixed(6)}, {result.lon.toFixed(6)}
</div> </div>
</div> </div>
@@ -178,9 +178,9 @@ export const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
{/* No results message */} {/* No results message */}
{showResults && !isLoading && query.length >= 3 && results.length === 0 && !error && ( {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"> <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}" No addresses found for "{query}"
</div> </div>
</CardBody> </CardBody>

View 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;

View 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;

View File

@@ -132,6 +132,30 @@
"last_30_days": "Last 30 days", "last_30_days": "Last 30 days",
"last_90_days": "Last 90 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": { "errors": {
"failed_to_load_stats": "Failed to load dashboard statistics. Please try again." "failed_to_load_stats": "Failed to load dashboard statistics. Please try again."
} }

View 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! 🥖🥐🍰"
}
}

View File

@@ -167,6 +167,30 @@
"last_30_days": "Últimos 30 días", "last_30_days": "Últimos 30 días",
"last_90_days": "Últimos 90 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": { "errors": {
"failed_to_load_stats": "Error al cargar las estadísticas del panel. Por favor, inténtelo de nuevo." "failed_to_load_stats": "Error al cargar las estadísticas del panel. Por favor, inténtelo de nuevo."
} }

View 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! 🥖🥐🍰"
}
}

View File

@@ -122,5 +122,29 @@
"last_7_days": "Azken 7 egun", "last_7_days": "Azken 7 egun",
"last_30_days": "Azken 30 egun", "last_30_days": "Azken 30 egun",
"last_90_days": "Azken 90 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"
}
} }
} }

View 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! 🥖🥐🍰"
}
}

View File

@@ -48,19 +48,17 @@ ONBOARDING_STEPS = [
# Phase 2: Core Setup # Phase 2: Core Setup
"setup", # Basic bakery setup and tenant creation "setup", # Basic bakery setup and tenant creation
# NOTE: POI detection now happens automatically in background during tenant registration
# Phase 2a: POI Detection (Location Context) # Phase 2a: AI-Assisted Inventory Setup (REFACTORED - split into 3 focused steps)
"poi-detection", # Detect nearby POIs for location-based ML features
# Phase 2b: AI-Assisted Inventory Setup (REFACTORED - split into 3 focused steps)
"upload-sales-data", # File upload, validation, and AI classification "upload-sales-data", # File upload, validation, and AI classification
"inventory-review", # Review and confirm AI-detected products with type selection "inventory-review", # Review and confirm AI-detected products with type selection
"initial-stock-entry", # Capture initial stock levels "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) "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 "suppliers-setup", # Suppliers configuration
# Phase 3: Advanced Configuration (all optional) # Phase 3: Advanced Configuration (all optional)
@@ -71,7 +69,7 @@ ONBOARDING_STEPS = [
# Phase 4: ML & Finalization # Phase 4: ML & Finalization
"ml-training", # AI model training "ml-training", # AI model training
"setup-review", # Review all configuration # "setup-review" removed - not useful for user, completion step is final
"completion" # Onboarding completed "completion" # Onboarding completed
] ]
@@ -83,9 +81,7 @@ STEP_DEPENDENCIES = {
# Core setup - no longer depends on data-source-choice (removed) # Core setup - no longer depends on data-source-choice (removed)
"setup": ["user_registered", "bakery-type-selection"], "setup": ["user_registered", "bakery-type-selection"],
# NOTE: POI detection removed from steps - now happens automatically in background
# POI Detection - requires tenant creation (setup)
"poi-detection": ["user_registered", "setup"],
# AI-Assisted Inventory Setup - REFACTORED into 3 sequential steps # AI-Assisted Inventory Setup - REFACTORED into 3 sequential steps
"upload-sales-data": ["user_registered", "setup"], "upload-sales-data": ["user_registered", "setup"],