2025-09-08 17:19:00 +02:00
import React , { useState , useRef } from 'react' ;
2025-10-19 19:22:37 +02:00
import { useTranslation } from 'react-i18next' ;
2025-09-08 17:19:00 +02:00
import { Button } from '../../../ui/Button' ;
import { Input } from '../../../ui/Input' ;
import { useCurrentTenant } from '../../../../stores/tenant.store' ;
2025-10-06 15:27:01 +02:00
import { useCreateIngredient , useClassifyBatch } from '../../../../api/hooks/inventory' ;
import { useValidateImportFile , useImportSalesData } from '../../../../api/hooks/sales' ;
2025-10-19 19:22:37 +02:00
import type { ImportValidationResponse } from '../../../../api/types/dataImport' ;
import type { ProductSuggestionResponse } from '../../../../api/types/inventory' ;
2025-09-08 22:28:26 +02:00
import { useAuth } from '../../../../contexts/AuthContext' ;
2025-09-08 17:19:00 +02:00
interface UploadSalesDataStepProps {
onNext : ( ) = > void ;
onPrevious : ( ) = > void ;
onComplete : ( data? : any ) = > void ;
isFirstStep : boolean ;
isLastStep : boolean ;
}
interface ProgressState {
stage : string ;
progress : number ;
message : string ;
}
interface InventoryItem {
suggestion_id : string ;
2025-09-08 21:44:04 +02:00
original_name : string ;
2025-09-08 17:19:00 +02:00
suggested_name : string ;
2025-09-08 21:44:04 +02:00
product_type : string ;
2025-09-08 17:19:00 +02:00
category : string ;
unit_of_measure : string ;
confidence_score : number ;
2025-09-08 21:44:04 +02:00
estimated_shelf_life_days? : number ;
2025-09-08 17:19:00 +02:00
requires_refrigeration : boolean ;
requires_freezing : boolean ;
is_seasonal : boolean ;
2025-09-08 21:44:04 +02:00
suggested_supplier? : string ;
2025-09-08 17:19:00 +02:00
notes? : string ;
2025-09-08 21:44:04 +02:00
sales_data ? : {
total_quantity : number ;
average_daily_sales : number ;
peak_day : string ;
frequency : number ;
} ;
// UI-specific fields
selected : boolean ;
stock_quantity : number ;
cost_per_unit : number ;
2025-09-08 17:19:00 +02:00
}
export const UploadSalesDataStep : React.FC < UploadSalesDataStepProps > = ( {
onPrevious ,
onComplete ,
isFirstStep
} ) = > {
2025-10-19 19:22:37 +02:00
const { t } = useTranslation ( ) ;
2025-09-08 17:19:00 +02:00
const [ selectedFile , setSelectedFile ] = useState < File | null > ( null ) ;
const [ isValidating , setIsValidating ] = useState ( false ) ;
const [ validationResult , setValidationResult ] = useState < ImportValidationResponse | null > ( null ) ;
const [ inventoryItems , setInventoryItems ] = useState < InventoryItem [ ] > ( [ ] ) ;
const [ showInventoryStep , setShowInventoryStep ] = useState ( false ) ;
const [ isCreating , setIsCreating ] = useState ( false ) ;
const [ error , setError ] = useState < string > ( '' ) ;
const [ progressState , setProgressState ] = useState < ProgressState | null > ( null ) ;
2025-10-19 19:22:37 +02:00
const [ showGuide , setShowGuide ] = useState ( false ) ;
2025-09-08 17:19:00 +02:00
const fileInputRef = useRef < HTMLInputElement > ( null ) ;
const currentTenant = useCurrentTenant ( ) ;
2025-09-08 22:28:26 +02:00
const { user } = useAuth ( ) ;
2025-10-06 15:27:01 +02:00
const validateFileMutation = useValidateImportFile ( ) ;
2025-09-08 17:19:00 +02:00
const createIngredient = useCreateIngredient ( ) ;
2025-10-06 15:27:01 +02:00
const importMutation = useImportSalesData ( ) ;
const classifyBatchMutation = useClassifyBatch ( ) ;
2025-09-08 17:19:00 +02:00
2025-09-08 22:28:26 +02:00
const handleFileSelect = async ( event : React.ChangeEvent < HTMLInputElement > ) = > {
2025-09-08 17:19:00 +02:00
const file = event . target . files ? . [ 0 ] ;
if ( file ) {
setSelectedFile ( file ) ;
setValidationResult ( null ) ;
setError ( '' ) ;
2025-09-08 22:28:26 +02:00
// Automatically trigger validation and classification
await handleAutoValidateAndClassify ( file ) ;
2025-09-08 17:19:00 +02:00
}
} ;
2025-09-08 22:28:26 +02:00
const handleDrop = async ( event : React.DragEvent < HTMLDivElement > ) = > {
2025-09-08 17:19:00 +02:00
event . preventDefault ( ) ;
const file = event . dataTransfer . files [ 0 ] ;
if ( file ) {
setSelectedFile ( file ) ;
setValidationResult ( null ) ;
setError ( '' ) ;
2025-09-08 22:28:26 +02:00
// Automatically trigger validation and classification
await handleAutoValidateAndClassify ( file ) ;
2025-09-08 17:19:00 +02:00
}
} ;
const handleDragOver = ( event : React.DragEvent < HTMLDivElement > ) = > {
event . preventDefault ( ) ;
} ;
2025-09-08 22:28:26 +02:00
const handleAutoValidateAndClassify = async ( file : File ) = > {
if ( ! currentTenant ? . id ) return ;
2025-09-08 17:19:00 +02:00
setIsValidating ( true ) ;
setError ( '' ) ;
2025-09-08 22:28:26 +02:00
setProgressState ( { stage : 'preparing' , progress : 0 , message : 'Preparando validación automática del archivo...' } ) ;
2025-09-08 17:19:00 +02:00
try {
2025-09-08 22:28:26 +02:00
// Step 1: Validate the file
2025-10-06 15:27:01 +02:00
const validationResult = await validateFileMutation . mutateAsync ( {
tenantId : currentTenant.id ,
file
} ) ;
// The API returns the validation result directly (not wrapped)
if ( validationResult && validationResult . is_valid !== undefined ) {
setValidationResult ( validationResult ) ;
2025-09-08 22:28:26 +02:00
setProgressState ( { stage : 'analyzing' , progress : 60 , message : 'Validación exitosa. Generando sugerencias automáticamente...' } ) ;
2025-10-06 15:27:01 +02:00
2025-09-08 22:28:26 +02:00
// Step 2: Automatically trigger classification
2025-10-06 15:27:01 +02:00
await generateInventorySuggestionsAuto ( validationResult ) ;
2025-09-08 17:19:00 +02:00
} else {
2025-10-06 15:27:01 +02:00
setError ( 'Respuesta de validación inválida del servidor' ) ;
2025-09-08 17:19:00 +02:00
setProgressState ( null ) ;
2025-09-08 22:28:26 +02:00
setIsValidating ( false ) ;
2025-09-08 17:19:00 +02:00
}
} catch ( error ) {
setError ( 'Error validando archivo: ' + ( error instanceof Error ? error . message : 'Error desconocido' ) ) ;
setProgressState ( null ) ;
2025-09-08 22:28:26 +02:00
setIsValidating ( false ) ;
2025-09-08 17:19:00 +02:00
}
} ;
2025-10-19 19:22:37 +02:00
const generateInventorySuggestionsAuto = async ( validationData : ImportValidationResponse ) = > {
2025-09-08 22:28:26 +02:00
if ( ! currentTenant ? . id ) {
2025-09-08 17:19:00 +02:00
setError ( 'No hay datos de validación disponibles para generar sugerencias' ) ;
2025-09-08 22:28:26 +02:00
setIsValidating ( false ) ;
setProgressState ( null ) ;
2025-09-08 17:19:00 +02:00
return ;
}
try {
2025-09-08 22:28:26 +02:00
setProgressState ( { stage : 'analyzing' , progress : 65 , message : 'Analizando productos de ventas...' } ) ;
2025-09-08 21:44:04 +02:00
// Extract product data from validation result - use the exact backend structure
2025-09-08 22:28:26 +02:00
const products = validationData . product_list ? . map ( ( productName : string ) = > ( {
2025-09-08 21:44:04 +02:00
product_name : productName
2025-09-08 17:19:00 +02:00
} ) ) || [ ] ;
if ( products . length === 0 ) {
setError ( 'No se encontraron productos en los datos de ventas' ) ;
setProgressState ( null ) ;
2025-09-08 22:28:26 +02:00
setIsValidating ( false ) ;
2025-09-08 17:19:00 +02:00
return ;
}
2025-09-08 22:28:26 +02:00
setProgressState ( { stage : 'classifying' , progress : 75 , message : 'Clasificando productos con IA...' } ) ;
2025-09-08 17:19:00 +02:00
// Call the classification API
2025-10-06 15:27:01 +02:00
const classificationResponse = await classifyBatchMutation . mutateAsync ( {
2025-09-08 17:19:00 +02:00
tenantId : currentTenant.id ,
2025-10-06 15:27:01 +02:00
products
2025-09-08 17:19:00 +02:00
} ) ;
2025-09-08 22:28:26 +02:00
setProgressState ( { stage : 'preparing' , progress : 90 , message : 'Preparando sugerencias de inventario...' } ) ;
2025-09-08 17:19:00 +02:00
2025-09-08 21:44:04 +02:00
// Convert API response to InventoryItem format - use exact backend structure plus UI fields
2025-10-19 19:22:37 +02:00
const items : InventoryItem [ ] = classificationResponse . suggestions . map ( ( suggestion : ProductSuggestionResponse ) = > {
2025-09-08 17:19:00 +02:00
// Calculate default stock quantity based on sales data
const defaultStock = Math . max (
Math . ceil ( ( suggestion . sales_data ? . average_daily_sales || 1 ) * 7 ) , // 1 week supply
1
) ;
// Estimate cost per unit based on category
const estimatedCost = suggestion . category === 'Dairy' ? 5.0 :
suggestion . category === 'Baking Ingredients' ? 2.0 :
3.0 ;
return {
2025-09-08 21:44:04 +02:00
// Exact backend fields
2025-09-08 17:19:00 +02:00
suggestion_id : suggestion.suggestion_id ,
2025-09-08 21:44:04 +02:00
original_name : suggestion.original_name ,
2025-09-08 17:19:00 +02:00
suggested_name : suggestion.suggested_name ,
2025-09-08 21:44:04 +02:00
product_type : suggestion.product_type ,
2025-09-08 17:19:00 +02:00
category : suggestion.category ,
unit_of_measure : suggestion.unit_of_measure ,
confidence_score : suggestion.confidence_score ,
2025-09-08 21:44:04 +02:00
estimated_shelf_life_days : suggestion.estimated_shelf_life_days ,
2025-09-08 17:19:00 +02:00
requires_refrigeration : suggestion.requires_refrigeration ,
requires_freezing : suggestion.requires_freezing ,
is_seasonal : suggestion.is_seasonal ,
2025-09-08 21:44:04 +02:00
suggested_supplier : suggestion.suggested_supplier ,
notes : suggestion.notes ,
sales_data : suggestion.sales_data ,
// UI-specific fields
selected : suggestion.confidence_score > 0.7 , // Auto-select high confidence items
stock_quantity : defaultStock ,
cost_per_unit : estimatedCost
2025-09-08 17:19:00 +02:00
} ;
} ) ;
setInventoryItems ( items ) ;
setShowInventoryStep ( true ) ;
setProgressState ( null ) ;
2025-09-08 22:28:26 +02:00
setIsValidating ( false ) ;
2025-09-08 17:19:00 +02:00
} catch ( err ) {
console . error ( 'Error generating inventory suggestions:' , err ) ;
setError ( 'Error al generar sugerencias de inventario. Por favor, inténtalo de nuevo.' ) ;
setProgressState ( null ) ;
2025-09-08 22:28:26 +02:00
setIsValidating ( false ) ;
2025-09-08 17:19:00 +02:00
}
} ;
2025-09-08 22:28:26 +02:00
2025-09-08 17:19:00 +02:00
const handleToggleSelection = ( id : string ) = > {
setInventoryItems ( items = >
items . map ( item = >
item . suggestion_id === id ? { . . . item , selected : ! item . selected } : item
)
) ;
} ;
const handleUpdateItem = ( id : string , field : keyof InventoryItem , value : number ) = > {
setInventoryItems ( items = >
items . map ( item = >
item . suggestion_id === id ? { . . . item , [ field ] : value } : item
)
) ;
} ;
const handleSelectAll = ( ) = > {
const allSelected = inventoryItems . every ( item = > item . selected ) ;
setInventoryItems ( items = >
items . map ( item = > ( { . . . item , selected : ! allSelected } ) )
) ;
} ;
const handleCreateInventory = async ( ) = > {
const selectedItems = inventoryItems . filter ( item = > item . selected ) ;
2025-10-15 21:09:42 +02:00
2025-09-08 17:19:00 +02:00
if ( selectedItems . length === 0 ) {
setError ( 'Por favor selecciona al menos un artículo de inventario para crear' ) ;
return ;
}
if ( ! currentTenant ? . id ) {
setError ( 'No se encontró información del tenant' ) ;
return ;
}
setIsCreating ( true ) ;
setError ( '' ) ;
try {
2025-10-15 21:09:42 +02:00
// Parallel inventory creation
setProgressState ( {
stage : 'creating_inventory' ,
progress : 10 ,
message : ` Creando ${ selectedItems . length } artículos de inventario... `
} ) ;
2025-09-08 17:19:00 +02:00
2025-10-15 21:09:42 +02:00
const creationPromises = selectedItems . map ( item = > {
2025-09-08 21:44:04 +02:00
const minimumStock = Math . max ( 1 , Math . ceil ( item . stock_quantity * 0.2 ) ) ;
const calculatedReorderPoint = Math . ceil ( item . stock_quantity * 0.3 ) ;
const reorderPoint = Math . max ( minimumStock + 2 , calculatedReorderPoint , minimumStock + 1 ) ;
2025-10-15 21:09:42 +02:00
2025-09-08 17:19:00 +02:00
const ingredientData = {
name : item.suggested_name ,
2025-11-05 13:34:56 +01:00
product_type : item.product_type ,
2025-09-08 17:19:00 +02:00
category : item.category ,
unit_of_measure : item.unit_of_measure ,
2025-09-08 21:44:04 +02:00
low_stock_threshold : minimumStock ,
max_stock_level : item.stock_quantity * 2 ,
reorder_point : reorderPoint ,
shelf_life_days : item.estimated_shelf_life_days || 30 ,
2025-09-08 17:19:00 +02:00
requires_refrigeration : item.requires_refrigeration ,
requires_freezing : item.requires_freezing ,
is_seasonal : item.is_seasonal ,
2025-09-08 21:44:04 +02:00
average_cost : item.cost_per_unit ,
2025-09-08 17:19:00 +02:00
notes : item.notes || ` Creado durante onboarding - Confianza: ${ Math . round ( item . confidence_score * 100 ) } % `
} ;
2025-10-15 21:09:42 +02:00
return createIngredient . mutateAsync ( {
2025-09-08 17:19:00 +02:00
tenantId : currentTenant.id ,
ingredientData
2025-10-15 21:09:42 +02:00
} ) . then ( created = > ( {
2025-09-08 17:19:00 +02:00
. . . created ,
initialStock : item.stock_quantity
2025-10-15 21:09:42 +02:00
} ) ) ;
} ) ;
const results = await Promise . allSettled ( creationPromises ) ;
const createdIngredients = results
. filter ( r = > r . status === 'fulfilled' )
. map ( r = > ( r as PromiseFulfilledResult < any > ) . value ) ;
const failedCount = results . filter ( r = > r . status === 'rejected' ) . length ;
if ( failedCount > 0 ) {
console . warn ( ` ${ failedCount } items failed to create out of ${ selectedItems . length } ` ) ;
2025-09-08 17:19:00 +02:00
}
2025-10-15 21:09:42 +02:00
console . log ( ` Successfully created ${ createdIngredients . length } inventory items in parallel ` ) ;
2025-09-08 17:19:00 +02:00
// After inventory creation, import the sales data
2025-10-15 21:09:42 +02:00
setProgressState ( {
stage : 'importing_sales' ,
progress : 50 ,
message : 'Importando datos de ventas...'
} ) ;
2025-09-08 17:19:00 +02:00
console . log ( 'Importing sales data after inventory creation...' ) ;
let salesImportResult = null ;
try {
if ( selectedFile ) {
2025-10-06 15:27:01 +02:00
const result = await importMutation . mutateAsync ( {
tenantId : currentTenant.id ,
file : selectedFile
} ) ;
2025-10-15 21:09:42 +02:00
2025-09-08 17:19:00 +02:00
salesImportResult = result ;
if ( result . success ) {
console . log ( 'Sales data imported successfully' ) ;
2025-10-15 21:09:42 +02:00
setProgressState ( {
stage : 'completing' ,
progress : 95 ,
message : 'Finalizando configuración...'
} ) ;
2025-09-08 17:19:00 +02:00
} else {
console . warn ( 'Sales import completed with issues:' , result . error ) ;
}
}
} catch ( importError ) {
console . error ( 'Error importing sales data:' , importError ) ;
}
setProgressState ( null ) ;
onComplete ( {
createdIngredients ,
2025-10-15 21:09:42 +02:00
totalItems : createdIngredients.length ,
2025-09-08 17:19:00 +02:00
validationResult ,
file : selectedFile ,
2025-09-08 22:28:26 +02:00
salesImportResult ,
2025-10-15 21:09:42 +02:00
inventoryConfigured : true ,
shouldAutoCompleteSuppliers : true ,
userId : user?.id
2025-09-08 17:19:00 +02:00
} ) ;
} catch ( err ) {
console . error ( 'Error creating inventory items:' , err ) ;
setError ( 'Error al crear artículos de inventario. Por favor, inténtalo de nuevo.' ) ;
setIsCreating ( false ) ;
setProgressState ( null ) ;
}
} ;
const formatFileSize = ( bytes : number ) = > {
if ( bytes === 0 ) return '0 Bytes' ;
const k = 1024 ;
const sizes = [ 'Bytes' , 'KB' , 'MB' , 'GB' ] ;
const i = Math . floor ( Math . log ( bytes ) / Math . log ( k ) ) ;
return parseFloat ( ( bytes / Math . pow ( k , i ) ) . toFixed ( 2 ) ) + ' ' + sizes [ i ] ;
} ;
const selectedCount = inventoryItems . filter ( item = > item . selected ) . length ;
const allSelected = inventoryItems . length > 0 && inventoryItems . every ( item = > item . selected ) ;
if ( showInventoryStep ) {
return (
< div className = "space-y-6" >
2025-11-06 14:36:10 +00:00
{ /* Why This Matters */ }
< div className = "bg-[var(--color-info)]/10 border border-[var(--color-info)]/20 rounded-lg p-4" >
< h3 className = "font-semibold text-[var(--text-primary)] mb-2 flex items-center gap-2" >
< svg className = "w-5 h-5 text-[var(--color-info)]" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" / >
< / svg >
{ t ( 'onboarding:ai_suggestions.why_title' , 'AI Smart Inventory' ) }
< / h3 >
< p className = "text-sm text-[var(--text-secondary)]" >
{ t ( 'onboarding:ai_suggestions.why_desc' , '¡Perfecto! Hemos analizado tus datos de ventas y generado sugerencias inteligentes de inventario. Selecciona los artículos que deseas agregar.' ) }
2025-09-08 17:19:00 +02:00
< / p >
< / div >
2025-11-06 14:36:10 +00:00
{ /* Progress indicator */ }
< div className = "flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg" >
< div className = "flex items-center gap-2" >
< svg className = "w-5 h-5 text-[var(--text-secondary)]" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" / >
< / svg >
< span className = "text-sm font-medium text-[var(--text-primary)]" >
{ selectedCount } de { inventoryItems . length } { t ( 'onboarding:ai_suggestions.items_selected' , 'artículos seleccionados' ) }
< / span >
< / div >
< div className = "flex items-center gap-2" >
{ selectedCount >= 1 && (
< div className = "flex items-center gap-1 text-xs text-[var(--color-success)]" >
< svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M5 13l4 4L19 7" / >
< / svg >
{ t ( 'onboarding:ai_suggestions.minimum_met' , 'Mínimo alcanzado' ) }
< / div >
) }
< button
type = "button"
2025-09-08 17:19:00 +02:00
onClick = { handleSelectAll }
2025-11-06 14:36:10 +00:00
className = "text-xs px-3 py-1.5 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded hover:border-[var(--color-primary)] hover:text-[var(--color-primary)] transition-colors"
2025-09-08 17:19:00 +02:00
>
2025-11-06 14:36:10 +00:00
{ allSelected ? t ( 'common:deselect_all' , 'Deseleccionar Todos' ) : t ( 'common:select_all' , 'Seleccionar Todos' ) }
< / button >
2025-09-08 17:19:00 +02:00
< / div >
< / div >
2025-11-06 14:36:10 +00:00
{ /* Product suggestions grid */ }
< div >
< h4 className = "text-sm font-medium text-[var(--text-secondary)] mb-3" >
{ t ( 'onboarding:ai_suggestions.suggested_products' , 'Productos Sugeridos' ) }
< / h4 >
< div className = "grid grid-cols-1 md:grid-cols-2 gap-3 max-h-96 overflow-y-auto" >
{ inventoryItems . map ( ( item ) = > (
< div
key = { item . suggestion_id }
onClick = { ( ) = > handleToggleSelection ( item . suggestion_id ) }
className = { ` p-4 border rounded-lg cursor-pointer transition-all ${
item . selected
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5 shadow-sm'
: 'border-[var(--border-secondary)] hover:border-[var(--border-primary)] hover:bg-[var(--bg-secondary)]'
} ` }
>
< div className = "flex items-start gap-3" >
{ /* Checkbox */ }
< div className = "flex-shrink-0 pt-1" >
< div
className = { ` w-5 h-5 rounded border-2 flex items-center justify-center transition-colors ${
item . selected
? 'border-[var(--color-primary)] bg-[var(--color-primary)]'
: 'border-[var(--border-secondary)]'
} ` }
>
{ item . selected && (
< svg className = "w-3 h-3 text-white" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 3 } d = "M5 13l4 4L19 7" / >
< / svg >
) }
< / div >
< / div >
2025-09-08 17:19:00 +02:00
2025-11-06 14:36:10 +00:00
{ /* Product info */ }
< div className = "flex-1 min-w-0" >
< h5 className = "font-medium text-[var(--text-primary)] truncate" >
2025-09-08 21:44:04 +02:00
{ item . suggested_name }
2025-11-06 14:36:10 +00:00
< / h5 >
< p className = "text-sm text-[var(--text-secondary)] mt-0.5" >
{ item . category } • { item . unit_of_measure }
2025-09-08 17:19:00 +02:00
< / p >
2025-11-06 14:36:10 +00:00
{ /* Tags */ }
< div className = "flex items-center gap-1.5 mt-2 flex-wrap" >
2025-11-06 14:09:10 +00:00
< span className = "text-xs bg-[var(--bg-primary)] px-2 py-0.5 rounded-full text-[var(--text-secondary)]" >
2025-11-06 14:36:10 +00:00
{ Math . round ( item . confidence_score * 100 ) } % confianza
2025-09-08 17:19:00 +02:00
< / span >
{ item . requires_refrigeration && (
2025-11-06 14:09:10 +00:00
< span className = "text-xs bg-[var(--color-info)]/10 text-[var(--color-info)] px-2 py-0.5 rounded-full" >
2025-11-06 14:36:10 +00:00
❄ ️ Refrigeración
2025-09-08 17:19:00 +02:00
< / span >
) }
2025-09-08 21:44:04 +02:00
{ item . requires_freezing && (
2025-11-06 14:09:10 +00:00
< span className = "text-xs bg-[var(--color-info)]/10 text-[var(--color-info)] px-2 py-0.5 rounded-full" >
2025-11-06 14:36:10 +00:00
🧊 Congelación
2025-09-08 21:44:04 +02:00
< / span >
) }
{ item . is_seasonal && (
2025-11-06 14:09:10 +00:00
< span className = "text-xs bg-[var(--color-success)]/10 text-[var(--color-success)] px-2 py-0.5 rounded-full" >
2025-11-06 14:36:10 +00:00
🌿 Estacional
2025-09-08 21:44:04 +02:00
< / span >
) }
2025-09-08 17:19:00 +02:00
< / div >
2025-11-06 14:36:10 +00:00
{ /* Sales data preview */ }
{ item . sales_data && (
< div className = "mt-2 pt-2 border-t border-[var(--border-secondary)]" >
< div className = "flex items-center gap-3 text-xs text-[var(--text-secondary)]" >
< span title = "Promedio diario" >
📊 { item . sales_data . average_daily_sales . toFixed ( 1 ) } / día
< / span >
< span title = "Cantidad total" >
📦 { item . sales_data . total_quantity } total
< / span >
< / div >
< / div >
) }
2025-09-08 17:19:00 +02:00
< / div >
2025-11-06 14:36:10 +00:00
< / div >
< / div >
) ) }
< / div >
< / div >
{ /* Edit selected items section */ }
{ selectedCount > 0 && (
< div className = "space-y-3 border-2 border-[var(--color-primary)] rounded-lg p-4 bg-gradient-to-br from-[var(--color-primary)]/5 to-transparent" >
< div className = "flex items-start justify-between" >
< div >
< h3 className = "font-semibold text-[var(--text-primary)] flex items-center gap-2" >
< svg className = "w-5 h-5 text-[var(--color-primary)]" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" / >
< / svg >
{ t ( 'onboarding:ai_suggestions.edit_details' , 'Configurar Detalles' ) }
< / h3 >
< p className = "text-sm text-[var(--text-secondary)] mt-1" >
{ t ( 'onboarding:ai_suggestions.edit_desc' , 'Ajusta el stock inicial y costos para los artículos seleccionados' ) }
< / p >
< / div >
< / div >
2025-09-08 17:19:00 +02:00
2025-11-06 14:36:10 +00:00
< div className = "space-y-3 max-h-80 overflow-y-auto" >
{ inventoryItems . filter ( item = > item . selected ) . map ( ( item ) = > (
< div
key = { item . suggestion_id }
className = "p-3 bg-[var(--bg-primary)] rounded-lg border border-[var(--border-secondary)]"
>
< div className = "font-medium text-[var(--text-primary)] mb-3 text-sm" >
{ item . suggested_name }
< / div >
< div className = "grid grid-cols-1 sm:grid-cols-3 gap-3" >
< div >
< label className = "block text-xs font-medium text-[var(--text-primary)] mb-1.5" >
{ t ( 'onboarding:ai_suggestions.initial_stock' , 'Stock Inicial' ) }
< / label >
< input
2025-09-08 17:19:00 +02:00
type = "number"
min = "0"
value = { item . stock_quantity . toString ( ) }
onChange = { ( e ) = > handleUpdateItem (
2025-09-08 21:44:04 +02:00
item . suggestion_id ,
2025-09-08 17:19:00 +02:00
'stock_quantity' ,
Number ( e . target . value )
) }
2025-11-06 14:36:10 +00:00
onClick = { ( e ) = > e . stopPropagation ( ) }
className = "w-full px-3 py-2 text-sm bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]"
placeholder = "0"
2025-09-08 17:19:00 +02:00
/ >
2025-11-06 14:36:10 +00:00
< / div >
< div >
< label className = "block text-xs font-medium text-[var(--text-primary)] mb-1.5" >
{ t ( 'onboarding:ai_suggestions.cost_per_unit' , 'Costo por Unidad' ) } ( € )
< / label >
< input
2025-09-08 17:19:00 +02:00
type = "number"
min = "0"
step = "0.01"
value = { item . cost_per_unit . toString ( ) }
onChange = { ( e ) = > handleUpdateItem (
2025-09-08 21:44:04 +02:00
item . suggestion_id ,
2025-09-08 17:19:00 +02:00
'cost_per_unit' ,
Number ( e . target . value )
) }
2025-11-06 14:36:10 +00:00
onClick = { ( e ) = > e . stopPropagation ( ) }
className = "w-full px-3 py-2 text-sm bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]"
placeholder = "0.00"
2025-09-08 17:19:00 +02:00
/ >
2025-11-06 14:36:10 +00:00
< / div >
< div >
< label className = "block text-xs font-medium text-[var(--text-primary)] mb-1.5" >
{ t ( 'onboarding:ai_suggestions.shelf_life_days' , 'Días de Caducidad' ) }
< / label >
< input
2025-09-08 17:19:00 +02:00
type = "number"
min = "1"
2025-09-08 21:44:04 +02:00
value = { ( item . estimated_shelf_life_days || 30 ) . toString ( ) }
2025-09-08 17:19:00 +02:00
onChange = { ( e ) = > handleUpdateItem (
2025-09-08 21:44:04 +02:00
item . suggestion_id ,
'estimated_shelf_life_days' ,
2025-09-08 17:19:00 +02:00
Number ( e . target . value )
) }
2025-11-06 14:36:10 +00:00
onClick = { ( e ) = > e . stopPropagation ( ) }
className = "w-full px-3 py-2 text-sm bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]"
placeholder = "30"
2025-09-08 17:19:00 +02:00
/ >
< / div >
2025-11-06 14:36:10 +00:00
< / div >
2025-09-08 17:19:00 +02:00
< / div >
2025-11-06 14:36:10 +00:00
) ) }
2025-09-08 17:19:00 +02:00
< / div >
2025-11-06 14:36:10 +00:00
< / div >
) }
2025-09-08 17:19:00 +02:00
{ error && (
< div className = "bg-[var(--color-error)]/10 border border-[var(--color-error)]/20 rounded-lg p-4" >
< p className = "text-[var(--color-error)]" > { error } < / p >
< / div >
) }
{ /* Actions */ }
2025-11-06 14:36:10 +00:00
< div className = "flex justify-end" >
2025-09-08 17:19:00 +02:00
< Button
onClick = { handleCreateInventory }
isLoading = { isCreating }
loadingText = "Creando Inventario..."
size = "lg"
disabled = { selectedCount === 0 }
2025-09-08 22:28:26 +02:00
className = "w-full sm:w-auto"
2025-09-08 17:19:00 +02:00
>
2025-09-08 21:44:04 +02:00
< span className = "hidden sm:inline" >
Crear { selectedCount } Artículo { selectedCount !== 1 ? 's' : '' } de Inventario
< / span >
< span className = "sm:hidden" >
Crear { selectedCount } Artículo { selectedCount !== 1 ? 's' : '' }
< / span >
2025-09-08 17:19:00 +02:00
< / Button >
< / div >
< / div >
) ;
}
return (
< div className = "space-y-6" >
< div className = "text-center" >
< p className = "text-[var(--text-secondary)] mb-6" >
2025-09-08 22:28:26 +02:00
Sube tus datos de ventas ( formato CSV o JSON ) y automáticamente validaremos y generaremos sugerencias de inventario inteligentes .
2025-09-08 17:19:00 +02:00
< / p >
< / div >
2025-10-19 19:22:37 +02:00
{ /* File Format Guide */ }
2025-11-06 14:36:10 +00:00
< div className = "bg-[var(--color-info)]/10 border border-[var(--color-info)]/20 rounded-lg p-4" >
2025-10-19 19:22:37 +02:00
< div className = "flex items-start justify-between" >
< div className = "flex items-center gap-2 mb-2" >
2025-11-06 14:36:10 +00:00
< svg className = "w-5 h-5 text-[var(--color-info)]" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
2025-10-19 19:22:37 +02:00
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" / >
< / svg >
2025-11-06 14:36:10 +00:00
< h3 className = "font-semibold text-[var(--text-primary)]" >
2025-10-19 19:22:37 +02:00
{ t ( 'onboarding:steps.inventory_setup.file_format_guide.title' , 'Guía de Formato de Archivo' ) }
< / h3 >
< / div >
< button
onClick = { ( ) = > setShowGuide ( ! showGuide ) }
2025-11-06 14:36:10 +00:00
className = "text-[var(--color-info)] hover:text-[var(--color-primary)] text-sm font-medium"
2025-10-19 19:22:37 +02:00
>
{ showGuide
? t ( 'onboarding:steps.inventory_setup.file_format_guide.collapse_guide' , 'Ocultar Guía' )
: t ( 'onboarding:steps.inventory_setup.file_format_guide.toggle_guide' , 'Ver Guía Completa' )
}
< / button >
< / div >
{ /* Quick Summary - Always Visible */ }
2025-11-06 14:36:10 +00:00
< div className = "text-sm text-[var(--text-secondary)] space-y-1" >
2025-10-19 19:22:37 +02:00
< p >
2025-11-06 14:36:10 +00:00
< strong className = "text-[var(--text-primary)]" > { t ( 'onboarding:steps.inventory_setup.file_format_guide.supported_formats.title' , 'Formatos Soportados' ) } : < / strong > { ' ' }
2025-10-19 19:22:37 +02:00
CSV , JSON , Excel ( XLSX ) • { t ( 'onboarding:steps.inventory_setup.file_format_guide.supported_formats.max_size' , 'Tamaño máximo: 10MB' ) }
< / p >
< p >
2025-11-06 14:36:10 +00:00
< strong className = "text-[var(--text-primary)]" > { t ( 'onboarding:steps.inventory_setup.file_format_guide.required_columns.title' , 'Columnas Requeridas' ) } : < / strong > { ' ' }
2025-10-19 19:22:37 +02:00
{ t ( 'onboarding:steps.inventory_setup.file_format_guide.required_columns.date' , 'Fecha' ) } , { ' ' }
{ t ( 'onboarding:steps.inventory_setup.file_format_guide.required_columns.product' , 'Nombre del Producto' ) } , { ' ' }
{ t ( 'onboarding:steps.inventory_setup.file_format_guide.required_columns.quantity' , 'Cantidad Vendida' ) }
< / p >
< / div >
{ /* Detailed Guide - Collapsible */ }
{ showGuide && (
2025-11-06 14:36:10 +00:00
< div className = "mt-4 pt-4 border-t border-[var(--border-secondary)] space-y-4" >
2025-10-19 19:22:37 +02:00
{ /* Required Columns Detail */ }
< div >
2025-11-06 14:36:10 +00:00
< h4 className = "font-semibold text-[var(--text-primary)] mb-2" >
2025-10-19 19:22:37 +02:00
{ t ( 'onboarding:steps.inventory_setup.file_format_guide.required_columns.title' , 'Columnas Requeridas' ) }
< / h4 >
2025-11-06 14:36:10 +00:00
< div className = "text-sm text-[var(--text-secondary)] space-y-1 pl-4" >
2025-10-19 19:22:37 +02:00
< p >
2025-11-06 14:36:10 +00:00
• < strong className = "text-[var(--text-primary)]" > { t ( 'onboarding:steps.inventory_setup.file_format_guide.required_columns.date' , 'Fecha' ) } : < / strong > { ' ' }
< span className = "font-mono text-xs bg-[var(--bg-secondary)] px-1.5 py-0.5 rounded" >
2025-10-19 19:22:37 +02:00
{ t ( 'onboarding:steps.inventory_setup.file_format_guide.required_columns.date_examples' , 'date, fecha, data' ) }
< / span >
< / p >
< p >
2025-11-06 14:36:10 +00:00
• < strong className = "text-[var(--text-primary)]" > { t ( 'onboarding:steps.inventory_setup.file_format_guide.required_columns.product' , 'Nombre del Producto' ) } : < / strong > { ' ' }
< span className = "font-mono text-xs bg-[var(--bg-secondary)] px-1.5 py-0.5 rounded" >
2025-10-19 19:22:37 +02:00
{ t ( 'onboarding:steps.inventory_setup.file_format_guide.required_columns.product_examples' , 'product, producto, product_name' ) }
< / span >
< / p >
< p >
2025-11-06 14:36:10 +00:00
• < strong className = "text-[var(--text-primary)]" > { t ( 'onboarding:steps.inventory_setup.file_format_guide.required_columns.quantity' , 'Cantidad Vendida' ) } : < / strong > { ' ' }
< span className = "font-mono text-xs bg-[var(--bg-secondary)] px-1.5 py-0.5 rounded" >
2025-10-19 19:22:37 +02:00
{ t ( 'onboarding:steps.inventory_setup.file_format_guide.required_columns.quantity_examples' , 'quantity, cantidad, quantity_sold' ) }
< / span >
< / p >
< / div >
< / div >
{ /* Optional Columns */ }
< div >
2025-11-06 14:36:10 +00:00
< h4 className = "font-semibold text-[var(--text-primary)] mb-2" >
2025-10-19 19:22:37 +02:00
{ t ( 'onboarding:steps.inventory_setup.file_format_guide.optional_columns.title' , 'Columnas Opcionales' ) }
< / h4 >
2025-11-06 14:36:10 +00:00
< div className = "text-sm text-[var(--text-secondary)] space-y-1 pl-4" >
2025-10-19 19:22:37 +02:00
< p > • { t ( 'onboarding:steps.inventory_setup.file_format_guide.optional_columns.revenue' , 'Ingresos (revenue, ingresos, ventas)' ) } < / p >
< p > • { t ( 'onboarding:steps.inventory_setup.file_format_guide.optional_columns.unit_price' , 'Precio Unitario (unit_price, precio, price)' ) } < / p >
< p > • { t ( 'onboarding:steps.inventory_setup.file_format_guide.optional_columns.category' , 'Categoría (category, categoria)' ) } < / p >
< p > • { t ( 'onboarding:steps.inventory_setup.file_format_guide.optional_columns.sku' , 'SKU del Producto' ) } < / p >
< p > • { t ( 'onboarding:steps.inventory_setup.file_format_guide.optional_columns.location' , 'Ubicación/Tienda' ) } < / p >
< / div >
< / div >
{ /* Date Formats */ }
< div >
2025-11-06 14:36:10 +00:00
< h4 className = "font-semibold text-[var(--text-primary)] mb-2" >
2025-10-19 19:22:37 +02:00
{ t ( 'onboarding:steps.inventory_setup.file_format_guide.date_formats.title' , 'Formatos de Fecha Soportados' ) }
< / h4 >
2025-11-06 14:36:10 +00:00
< div className = "text-sm text-[var(--text-secondary)] pl-4" >
2025-10-19 19:22:37 +02:00
< p > { t ( 'onboarding:steps.inventory_setup.file_format_guide.date_formats.formats' , 'YYYY-MM-DD, DD/MM/YYYY, MM/DD/YYYY, DD-MM-YYYY, y más' ) } < / p >
< p className = "text-xs mt-1" > { t ( 'onboarding:steps.inventory_setup.file_format_guide.date_formats.with_time' , 'También se admiten formatos con hora' ) } < / p >
< / div >
< / div >
{ /* Automatic Features */ }
< div >
2025-11-06 14:36:10 +00:00
< h4 className = "font-semibold text-[var(--text-primary)] mb-2" >
2025-10-19 19:22:37 +02:00
{ t ( 'onboarding:steps.inventory_setup.file_format_guide.features.title' , 'Características Automáticas' ) }
< / h4 >
2025-11-06 14:36:10 +00:00
< div className = "text-sm text-[var(--text-secondary)] space-y-1 pl-4" >
2025-10-19 19:22:37 +02:00
< p > ✓ { t ( 'onboarding:steps.inventory_setup.file_format_guide.features.multilingual' , 'Detección multiidioma de columnas' ) } < / p >
< p > ✓ { t ( 'onboarding:steps.inventory_setup.file_format_guide.features.validation' , 'Validación automática con reporte detallado' ) } < / p >
< p > ✓ { t ( 'onboarding:steps.inventory_setup.file_format_guide.features.ai_classification' , 'Clasificación de productos con IA' ) } < / p >
< p > ✓ { t ( 'onboarding:steps.inventory_setup.file_format_guide.features.inventory_suggestions' , 'Sugerencias inteligentes de inventario' ) } < / p >
< / div >
< / div >
< / div >
) }
< / div >
2025-09-08 17:19:00 +02:00
{ /* File Upload Area */ }
< div
className = { ` border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
selectedFile
? 'border-[var(--color-success)] bg-[var(--color-success)]/5'
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)] hover:bg-[var(--color-primary)]/5'
} ` }
onDrop = { handleDrop }
onDragOver = { handleDragOver }
>
< input
ref = { fileInputRef }
type = "file"
accept = ".csv,.json"
onChange = { handleFileSelect }
className = "hidden"
/ >
{ selectedFile ? (
< div className = "space-y-4" >
< div className = "text-[var(--color-success)]" >
< svg className = "mx-auto h-12 w-12 mb-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" / >
< / svg >
< p className = "text-lg font-medium" > Archivo Seleccionado < / p >
< p className = "text-[var(--text-secondary)]" > { selectedFile . name } < / p >
< p className = "text-sm text-[var(--text-tertiary)]" >
{ formatFileSize ( selectedFile . size ) }
< / p >
< / div >
< Button
variant = "outline"
onClick = { ( ) = > fileInputRef . current ? . click ( ) }
>
Choose Different File
< / Button >
< / div >
) : (
< div className = "space-y-4" >
< svg className = "mx-auto h-12 w-12 text-[var(--text-tertiary)]" stroke = "currentColor" fill = "none" viewBox = "0 0 48 48" >
< path d = "M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" strokeWidth = { 2 } strokeLinecap = "round" strokeLinejoin = "round" / >
< / svg >
< div >
< p className = "text-lg font-medium" > Drop your sales data here < / p >
< p className = "text-[var(--text-secondary)]" > or click to browse files < / p >
< p className = "text-sm text-[var(--text-tertiary)] mt-2" >
2025-09-08 22:28:26 +02:00
Supported formats : CSV , JSON ( max 100 MB ) < br / >
< span className = "text-[var(--color-primary)]" > Auto - validates and generates suggestions < / span >
2025-09-08 17:19:00 +02:00
< / p >
< / div >
< Button
variant = "outline"
onClick = { ( ) = > fileInputRef . current ? . click ( ) }
>
Choose File
< / Button >
< / div >
) }
< / div >
{ /* Progress */ }
{ progressState && (
< div className = "bg-[var(--bg-secondary)] rounded-lg p-4" >
< div className = "flex justify-between text-sm mb-2" >
< span className = "font-medium" > { progressState . message } < / span >
< span > { progressState . progress } % < / span >
< / div >
< div className = "w-full bg-[var(--bg-tertiary)] rounded-full h-2" >
< div
className = "bg-[var(--color-primary)] h-2 rounded-full transition-all duration-300"
style = { { width : ` ${ progressState . progress } % ` } }
/ >
< / div >
< / div >
) }
{ /* Validation Results */ }
{ validationResult && (
< div className = "bg-[var(--color-success)]/10 border border-[var(--color-success)]/20 rounded-lg p-4" >
< h3 className = "font-semibold text-[var(--color-success)] mb-2" > Validation Successful ! < / h3 >
< div className = "space-y-2 text-sm" >
< p > Total records : { validationResult . total_records } < / p >
< p > Valid records : { validationResult . valid_records } < / p >
{ validationResult . invalid_records > 0 && (
< p className = "text-[var(--color-warning)]" >
Invalid records : { validationResult . invalid_records }
< / p >
) }
{ validationResult . warnings && validationResult . warnings . length > 0 && (
< div className = "mt-2" >
< p className = "font-medium text-[var(--color-warning)]" > Warnings : < / p >
< ul className = "list-disc list-inside" >
2025-10-19 19:22:37 +02:00
{ validationResult . warnings . map ( ( warning : any , index : number ) = > (
2025-09-08 21:44:04 +02:00
< li key = { index } className = "text-[var(--color-warning)]" >
{ typeof warning === 'string' ? warning : JSON.stringify ( warning ) }
< / li >
2025-09-08 17:19:00 +02:00
) ) }
< / ul >
< / div >
) }
< / div >
< / div >
) }
{ /* Error */ }
{ error && (
< div className = "bg-[var(--color-error)]/10 border border-[var(--color-error)]/20 rounded-lg p-4" >
< p className = "text-[var(--color-error)]" > { error } < / p >
< / div >
) }
2025-11-06 14:36:10 +00:00
{ /* Status indicator */ }
{ selectedFile && ! showInventoryStep && (
< div className = "flex items-center justify-center px-4 py-2 bg-[var(--bg-secondary)] rounded-lg" >
{ isValidating ? (
< >
< div className = "animate-spin rounded-full h-4 w-4 border-b-2 border-[var(--color-primary)] mr-2" > < / div >
< span className = "text-sm text-[var(--text-secondary)]" > Procesando automáticamente . . . < / span >
< / >
) : validationResult ? (
< >
< svg className = "w-4 h-4 text-[var(--color-success)] mr-2" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M5 13l4 4L19 7" / >
< / svg >
< span className = "text-sm text-[var(--color-success)]" > Archivo procesado exitosamente < / span >
< / >
) : null }
< / div >
) }
2025-09-08 17:19:00 +02:00
< / div >
) ;
} ;