diff --git a/frontend/src/components/domain/onboarding/steps/InitialStockEntryStep.tsx b/frontend/src/components/domain/onboarding/steps/InitialStockEntryStep.tsx new file mode 100644 index 00000000..8fabde29 --- /dev/null +++ b/frontend/src/components/domain/onboarding/steps/InitialStockEntryStep.tsx @@ -0,0 +1,285 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Package, Salad, AlertCircle, ArrowRight, ArrowLeft, CheckCircle } from 'lucide-react'; +import Button from '../../../ui/Button/Button'; +import Card from '../../../ui/Card/Card'; +import Input from '../../../ui/Input/Input'; + +export interface ProductWithStock { + id: string; + name: string; + type: 'ingredient' | 'finished_product'; + category?: string; + unit?: string; + initialStock?: number; +} + +export interface InitialStockEntryStepProps { + products: ProductWithStock[]; + onUpdate?: (data: { productsWithStock: ProductWithStock[] }) => void; + onComplete?: () => void; + onPrevious?: () => void; + initialData?: { + productsWithStock?: ProductWithStock[]; + }; +} + +export const InitialStockEntryStep: React.FC = ({ + products: initialProducts, + onUpdate, + onComplete, + onPrevious, + initialData, +}) => { + const { t } = useTranslation(); + const [products, setProducts] = useState(() => { + if (initialData?.productsWithStock) { + return initialData.productsWithStock; + } + return initialProducts.map(p => ({ + ...p, + initialStock: p.initialStock ?? undefined, + })); + }); + + const ingredients = products.filter(p => p.type === 'ingredient'); + const finishedProducts = products.filter(p => p.type === 'finished_product'); + + const handleStockChange = (productId: string, value: string) => { + const numValue = value === '' ? undefined : parseFloat(value); + const updatedProducts = products.map(p => + p.id === productId ? { ...p, initialStock: numValue } : p + ); + + setProducts(updatedProducts); + onUpdate?.({ productsWithStock: updatedProducts }); + }; + + const handleSetAllToZero = () => { + const updatedProducts = products.map(p => ({ ...p, initialStock: 0 })); + setProducts(updatedProducts); + onUpdate?.({ productsWithStock: updatedProducts }); + }; + + const handleSkipForNow = () => { + // Set all undefined values to 0 + const updatedProducts = products.map(p => ({ + ...p, + initialStock: p.initialStock ?? 0, + })); + setProducts(updatedProducts); + onUpdate?.({ productsWithStock: updatedProducts }); + onComplete?.(); + }; + + const handleContinue = () => { + onComplete?.(); + }; + + const productsWithStock = products.filter(p => p.initialStock !== undefined && p.initialStock >= 0); + const productsWithoutStock = products.filter(p => p.initialStock === undefined); + const completionPercentage = (productsWithStock.length / products.length) * 100; + const allCompleted = productsWithoutStock.length === 0; + + return ( +
+ {/* Header */} +
+

+ {t('onboarding:stock.title', 'Niveles de Stock Inicial')} +

+

+ {t( + 'onboarding:stock.subtitle', + 'Ingresa las cantidades actuales de cada producto. Esto permite que el sistema rastree el inventario desde hoy.' + )} +

+
+ + {/* Info Banner */} + +
+ +
+

+ {t('onboarding:stock.info_title', '¿Por qué es importante?')} +

+

+ {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.' + )} +

+
+
+
+ + {/* Progress */} +
+
+ + {t('onboarding:stock.progress', 'Progreso de captura')} + + + {productsWithStock.length} / {products.length} + +
+
+
+
+
+ + {/* Quick Actions */} +
+ + +
+ + {/* Ingredients Section */} + {ingredients.length > 0 && ( +
+
+
+ +
+

+ {t('onboarding:stock.ingredients', 'Ingredientes')} ({ingredients.length}) +

+
+ +
+ {ingredients.map(product => { + const hasStock = product.initialStock !== undefined; + return ( + +
+
+
+
+ {product.name} + {hasStock && } +
+ {product.category && ( +
{product.category}
+ )} +
+
+ handleStockChange(product.id, e.target.value)} + placeholder="0" + min="0" + step="0.01" + className="w-24 text-right" + /> + + {product.unit || 'kg'} + +
+
+
+
+ ); + })} +
+
+ )} + + {/* Finished Products Section */} + {finishedProducts.length > 0 && ( +
+
+
+ +
+

+ {t('onboarding:stock.finished_products', 'Productos Terminados')} ({finishedProducts.length}) +

+
+ +
+ {finishedProducts.map(product => { + const hasStock = product.initialStock !== undefined; + return ( + +
+
+
+
+ {product.name} + {hasStock && } +
+ {product.category && ( +
{product.category}
+ )} +
+
+ handleStockChange(product.id, e.target.value)} + placeholder="0" + min="0" + step="1" + className="w-24 text-right" + /> + + {product.unit || t('common:units', 'unidades')} + +
+
+
+
+ ); + })} +
+
+ )} + + {/* Warning for incomplete */} + {!allCompleted && ( + +
+ +
+

+ {t('onboarding:stock.incomplete_warning', 'Faltan {count} productos por completar', { + count: productsWithoutStock.length, + })} +

+

+ {t( + 'onboarding:stock.incomplete_help', + 'Puedes continuar, pero recomendamos ingresar todas las cantidades para un mejor control de inventario.' + )} +

+
+
+
+ )} + + {/* Footer Actions */} +
+ + + +
+
+ ); +}; + +export default InitialStockEntryStep; diff --git a/frontend/src/components/domain/onboarding/steps/ProductCategorizationStep.tsx b/frontend/src/components/domain/onboarding/steps/ProductCategorizationStep.tsx new file mode 100644 index 00000000..6762fe05 --- /dev/null +++ b/frontend/src/components/domain/onboarding/steps/ProductCategorizationStep.tsx @@ -0,0 +1,364 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Package, Salad, ArrowRight, ArrowLeft, Info } from 'lucide-react'; +import Button from '../../../ui/Button/Button'; +import Card from '../../../ui/Card/Card'; + +export interface Product { + id: string; + name: string; + category?: string; + confidence?: number; + type?: 'ingredient' | 'finished_product' | null; + suggestedType?: 'ingredient' | 'finished_product'; +} + +export interface ProductCategorizationStepProps { + products: Product[]; + onUpdate?: (data: { categorizedProducts: Product[] }) => void; + onComplete?: () => void; + onPrevious?: () => void; + initialData?: { + categorizedProducts?: Product[]; + }; +} + +export const ProductCategorizationStep: React.FC = ({ + products: initialProducts, + onUpdate, + onComplete, + onPrevious, + initialData, +}) => { + const { t } = useTranslation(); + const [products, setProducts] = useState(() => { + if (initialData?.categorizedProducts) { + return initialData.categorizedProducts; + } + return initialProducts.map(p => ({ + ...p, + type: p.suggestedType || null, + })); + }); + + const [draggedProduct, setDraggedProduct] = useState(null); + + const uncategorizedProducts = products.filter(p => !p.type); + const ingredients = products.filter(p => p.type === 'ingredient'); + const finishedProducts = products.filter(p => p.type === 'finished_product'); + + const handleDragStart = (product: Product) => { + setDraggedProduct(product); + }; + + const handleDragEnd = () => { + setDraggedProduct(null); + }; + + const handleDrop = (type: 'ingredient' | 'finished_product') => { + if (!draggedProduct) return; + + const updatedProducts = products.map(p => + p.id === draggedProduct.id ? { ...p, type } : p + ); + + setProducts(updatedProducts); + onUpdate?.({ categorizedProducts: updatedProducts }); + setDraggedProduct(null); + }; + + const handleMoveProduct = (productId: string, type: 'ingredient' | 'finished_product' | null) => { + const updatedProducts = products.map(p => + p.id === productId ? { ...p, type } : p + ); + + setProducts(updatedProducts); + onUpdate?.({ categorizedProducts: updatedProducts }); + }; + + const handleAcceptAllSuggestions = () => { + const updatedProducts = products.map(p => ({ + ...p, + type: p.suggestedType || p.type, + })); + + setProducts(updatedProducts); + onUpdate?.({ categorizedProducts: updatedProducts }); + }; + + const handleContinue = () => { + onComplete?.(); + }; + + const allCategorized = uncategorizedProducts.length === 0; + const categorizationProgress = ((products.length - uncategorizedProducts.length) / products.length) * 100; + + return ( +
+ {/* Header */} +
+

+ {t('onboarding:categorization.title', 'Categoriza tus Productos')} +

+

+ {t( + 'onboarding:categorization.subtitle', + 'Ayúdanos a entender qué son ingredientes (para usar en recetas) y qué son productos terminados (para vender)' + )} +

+
+ + {/* Info Banner */} + +
+ +
+

+ {t('onboarding:categorization.info_title', '¿Por qué es importante?')} +

+

+ {t( + 'onboarding:categorization.info_text', + 'Los ingredientes se usan en recetas para crear productos. Los productos terminados se venden directamente. Esta clasificación permite calcular costos y planificar producción correctamente.' + )} +

+
+
+
+ + {/* Progress Bar */} +
+
+ + {t('onboarding:categorization.progress', 'Progreso de categorización')} + + + {products.length - uncategorizedProducts.length} / {products.length} + +
+
+
+
+
+ + {/* Quick Actions */} + {uncategorizedProducts.length > 0 && products.some(p => p.suggestedType) && ( +
+ +
+ )} + + {/* Categorization Areas */} +
+ {/* Uncategorized */} + {uncategorizedProducts.length > 0 && ( + +
+
+
+ 📦 +
+
+

+ {t('onboarding:categorization.uncategorized', 'Sin Categorizar')} +

+

+ {uncategorizedProducts.length} {t('common:items', 'items')} +

+
+
+ +
+ {uncategorizedProducts.map(product => ( +
handleDragStart(product)} + onDragEnd={handleDragEnd} + className="p-3 bg-white border border-gray-200 rounded-lg cursor-move hover:shadow-md transition-all" + > +
{product.name}
+ {product.category && ( +
{product.category}
+ )} + {product.suggestedType && ( +
+ + {t( + `onboarding:categorization.suggested_${product.suggestedType}`, + product.suggestedType === 'ingredient' ? 'Sugerido: Ingrediente' : 'Sugerido: Producto' + )} +
+ )} +
+ + +
+
+ ))} +
+
+
+ )} + + {/* Ingredients */} + e.preventDefault()} + onDrop={() => handleDrop('ingredient')} + > +
+
+
+ +
+
+

+ {t('onboarding:categorization.ingredients_title', 'Ingredientes')} +

+

+ {ingredients.length} {t('common:items', 'items')} +

+
+
+ +

+ {t('onboarding:categorization.ingredients_help', 'Para usar en recetas')} +

+ +
+ {ingredients.length === 0 && ( +
+ {t('onboarding:categorization.drag_here', 'Arrastra productos aquí')} +
+ )} + {ingredients.map(product => ( +
+
+
+
{product.name}
+ {product.category && ( +
{product.category}
+ )} +
+ +
+
+ ))} +
+
+
+ + {/* Finished Products */} + e.preventDefault()} + onDrop={() => handleDrop('finished_product')} + > +
+
+
+ +
+
+

+ {t('onboarding:categorization.finished_products_title', 'Productos Terminados')} +

+

+ {finishedProducts.length} {t('common:items', 'items')} +

+
+
+ +

+ {t('onboarding:categorization.finished_products_help', 'Para vender directamente')} +

+ +
+ {finishedProducts.length === 0 && ( +
+ {t('onboarding:categorization.drag_here', 'Arrastra productos aquí')} +
+ )} + {finishedProducts.map(product => ( +
+
+
+
{product.name}
+ {product.category && ( +
{product.category}
+ )} +
+ +
+
+ ))} +
+
+
+
+ + {/* Footer Actions */} +
+ + +
+ {!allCategorized && ( +

+ {t( + 'onboarding:categorization.incomplete_warning', + '⚠️ Categoriza todos los productos para continuar' + )} +

+ )} +
+ + +
+
+ ); +}; + +export default ProductCategorizationStep; diff --git a/frontend/src/components/domain/onboarding/steps/index.ts b/frontend/src/components/domain/onboarding/steps/index.ts index 44fb331d..e25b3feb 100644 --- a/frontend/src/components/domain/onboarding/steps/index.ts +++ b/frontend/src/components/domain/onboarding/steps/index.ts @@ -6,6 +6,10 @@ export { default as DataSourceChoiceStep } from './DataSourceChoiceStep'; export { RegisterTenantStep } from './RegisterTenantStep'; export { UploadSalesDataStep } from './UploadSalesDataStep'; +// AI-Assisted Path Steps +export { default as ProductCategorizationStep } from './ProductCategorizationStep'; +export { default as InitialStockEntryStep } from './InitialStockEntryStep'; + // Production Steps export { default as ProductionProcessesStep } from './ProductionProcessesStep'; diff --git a/frontend/src/locales/es/onboarding.json b/frontend/src/locales/es/onboarding.json index 12f8b5a7..f8c917ec 100644 --- a/frontend/src/locales/es/onboarding.json +++ b/frontend/src/locales/es/onboarding.json @@ -361,5 +361,39 @@ "cancel": "Cancelar", "add": "Agregar Proceso" } + }, + "categorization": { + "title": "Categoriza tus Productos", + "subtitle": "Ayúdanos a entender qué son ingredientes (para usar en recetas) y qué son productos terminados (para vender)", + "info_title": "¿Por qué es importante?", + "info_text": "Los ingredientes se usan en recetas para crear productos. Los productos terminados se venden directamente. Esta clasificación permite calcular costos y planificar producción correctamente.", + "progress": "Progreso de categorización", + "accept_all_suggestions": "⚡ Aceptar todas las sugerencias de IA", + "uncategorized": "Sin Categorizar", + "ingredients_title": "Ingredientes", + "ingredients_help": "Para usar en recetas", + "finished_products_title": "Productos Terminados", + "finished_products_help": "Para vender directamente", + "drag_here": "Arrastra productos aquí", + "ingredient": "Ingrediente", + "finished_product": "Producto", + "suggested_ingredient": "Sugerido: Ingrediente", + "suggested_finished_product": "Sugerido: Producto", + "incomplete_warning": "⚠️ Categoriza todos los productos para continuar" + }, + "stock": { + "title": "Niveles de Stock Inicial", + "subtitle": "Ingresa las cantidades actuales de cada producto. Esto permite que el sistema rastree el inventario desde hoy.", + "info_title": "¿Por qué es importante?", + "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.", + "progress": "Progreso de captura", + "set_all_zero": "Establecer todo a 0", + "skip_for_now": "Omitir por ahora (se establecerá a 0)", + "ingredients": "Ingredientes", + "finished_products": "Productos Terminados", + "incomplete_warning": "Faltan {{count}} productos por completar", + "incomplete_help": "Puedes continuar, pero recomendamos ingresar todas las cantidades para un mejor control de inventario.", + "complete": "Completar Configuración", + "continue_anyway": "Continuar de todos modos" } } \ No newline at end of file