2025-11-09 09:22:08 +01:00
import React , { useState , useEffect } from 'react' ;
import { useTranslation } from 'react-i18next' ;
import { Button } from '../../../ui/Button' ;
import { useCurrentTenant } from '../../../../stores/tenant.store' ;
import { useCreateIngredient } from '../../../../api/hooks/inventory' ;
import { useImportSalesData } from '../../../../api/hooks/sales' ;
import type { ProductSuggestionResponse , IngredientCreate } from '../../../../api/types/inventory' ;
import { ProductType , UnitOfMeasure , IngredientCategory , ProductCategory } from '../../../../api/types/inventory' ;
import { Package , ShoppingBag , AlertCircle , CheckCircle2 , Edit2 , Trash2 , Plus , Sparkles } from 'lucide-react' ;
interface InventoryReviewStepProps {
onNext : ( ) = > void ;
onPrevious : ( ) = > void ;
onComplete : ( data : {
inventoryItemsCreated : number ;
salesDataImported : boolean ;
} ) = > void ;
isFirstStep : boolean ;
isLastStep : boolean ;
initialData ? : {
uploadedFile? : File ; // NEW: File object for sales import
validationResult? : any ; // NEW: Validation result
aiSuggestions : ProductSuggestionResponse [ ] ;
uploadedFileName : string ;
uploadedFileSize : number ;
} ;
}
interface InventoryItemForm {
id : string ; // Unique ID for UI tracking
name : string ;
product_type : ProductType ;
category : string ;
unit_of_measure : UnitOfMeasure | string ;
// AI suggestion metadata (if from AI)
isSuggested : boolean ;
confidence_score? : number ;
sales_data ? : {
total_quantity : number ;
average_daily_sales : number ;
} ;
}
type FilterType = 'all' | 'ingredients' | 'finished_products' ;
// Template Definitions - Common Bakery Ingredients
interface TemplateItem {
name : string ;
product_type : ProductType ;
category : string ;
unit_of_measure : UnitOfMeasure ;
}
interface IngredientTemplate {
id : string ;
name : string ;
description : string ;
icon : string ;
items : TemplateItem [ ] ;
}
const INGREDIENT_TEMPLATES : IngredientTemplate [ ] = [
{
id : 'basic-bakery' ,
name : 'Ingredientes Básicos de Panadería' ,
description : 'Esenciales para cualquier panadería' ,
icon : '🍞' ,
items : [
{ name : 'Harina de Trigo' , product_type : ProductType.INGREDIENT , category : IngredientCategory.FLOUR , unit_of_measure : UnitOfMeasure.KILOGRAMS } ,
{ name : 'Azúcar' , product_type : ProductType.INGREDIENT , category : IngredientCategory.SUGAR , unit_of_measure : UnitOfMeasure.KILOGRAMS } ,
{ name : 'Sal' , product_type : ProductType.INGREDIENT , category : IngredientCategory.SALT , unit_of_measure : UnitOfMeasure.KILOGRAMS } ,
{ name : 'Levadura Fresca' , product_type : ProductType.INGREDIENT , category : IngredientCategory.YEAST , unit_of_measure : UnitOfMeasure.KILOGRAMS } ,
{ name : 'Agua' , product_type : ProductType.INGREDIENT , category : IngredientCategory.OTHER , unit_of_measure : UnitOfMeasure.LITERS } ,
] ,
} ,
{
id : 'pastry-essentials' ,
name : 'Esenciales para Pastelería' ,
description : 'Ingredientes para pasteles y postres' ,
icon : '🎂' ,
items : [
{ name : 'Huevos' , product_type : ProductType.INGREDIENT , category : IngredientCategory.EGGS , unit_of_measure : UnitOfMeasure.UNITS } ,
{ name : 'Mantequilla' , product_type : ProductType.INGREDIENT , category : IngredientCategory.FATS , unit_of_measure : UnitOfMeasure.KILOGRAMS } ,
{ name : 'Leche' , product_type : ProductType.INGREDIENT , category : IngredientCategory.DAIRY , unit_of_measure : UnitOfMeasure.LITERS } ,
{ name : 'Vainilla' , product_type : ProductType.INGREDIENT , category : IngredientCategory.SPICES , unit_of_measure : UnitOfMeasure.MILLILITERS } ,
{ name : 'Azúcar Glass' , product_type : ProductType.INGREDIENT , category : IngredientCategory.SUGAR , unit_of_measure : UnitOfMeasure.KILOGRAMS } ,
] ,
} ,
{
id : 'bread-basics' ,
name : 'Básicos para Pan Artesanal' ,
description : 'Todo lo necesario para pan artesanal' ,
icon : '🥖' ,
items : [
{ name : 'Harina Integral' , product_type : ProductType.INGREDIENT , category : IngredientCategory.FLOUR , unit_of_measure : UnitOfMeasure.KILOGRAMS } ,
{ name : 'Masa Madre' , product_type : ProductType.INGREDIENT , category : IngredientCategory.YEAST , unit_of_measure : UnitOfMeasure.KILOGRAMS } ,
{ name : 'Aceite de Oliva' , product_type : ProductType.INGREDIENT , category : IngredientCategory.FATS , unit_of_measure : UnitOfMeasure.LITERS } ,
{ name : 'Semillas de Sésamo' , product_type : ProductType.INGREDIENT , category : IngredientCategory.OTHER , unit_of_measure : UnitOfMeasure.KILOGRAMS } ,
] ,
} ,
{
id : 'chocolate-specialties' ,
name : 'Especialidades de Chocolate' ,
description : 'Para productos con chocolate' ,
icon : '🍫' ,
items : [
{ name : 'Chocolate Negro' , product_type : ProductType.INGREDIENT , category : IngredientCategory.OTHER , unit_of_measure : UnitOfMeasure.KILOGRAMS } ,
{ name : 'Cacao en Polvo' , product_type : ProductType.INGREDIENT , category : IngredientCategory.OTHER , unit_of_measure : UnitOfMeasure.KILOGRAMS } ,
{ name : 'Chocolate con Leche' , product_type : ProductType.INGREDIENT , category : IngredientCategory.OTHER , unit_of_measure : UnitOfMeasure.KILOGRAMS } ,
{ name : 'Crema de Avellanas' , product_type : ProductType.INGREDIENT , category : IngredientCategory.OTHER , unit_of_measure : UnitOfMeasure.KILOGRAMS } ,
] ,
} ,
] ;
export const InventoryReviewStep : React.FC < InventoryReviewStepProps > = ( {
onComplete ,
onPrevious ,
isFirstStep ,
initialData
} ) = > {
const { t } = useTranslation ( ) ;
const [ inventoryItems , setInventoryItems ] = useState < InventoryItemForm [ ] > ( [ ] ) ;
const [ activeFilter , setActiveFilter ] = useState < FilterType > ( 'all' ) ;
const [ isAdding , setIsAdding ] = useState ( false ) ;
const [ editingId , setEditingId ] = useState < string | null > ( null ) ;
const [ formData , setFormData ] = useState < InventoryItemForm > ( {
id : '' ,
name : '' ,
product_type : ProductType.INGREDIENT ,
category : '' ,
unit_of_measure : UnitOfMeasure.KILOGRAMS ,
isSuggested : false ,
} ) ;
const [ formErrors , setFormErrors ] = useState < Record < string , string > > ( { } ) ;
const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
const currentTenant = useCurrentTenant ( ) ;
const tenantId = currentTenant ? . id || '' ;
// API hooks
const createIngredientMutation = useCreateIngredient ( ) ;
const importSalesMutation = useImportSalesData ( ) ;
// Initialize with AI suggestions
useEffect ( ( ) = > {
if ( initialData ? . aiSuggestions ) {
const items : InventoryItemForm [ ] = initialData . aiSuggestions . map ( ( suggestion , index ) = > ( {
id : ` ai- ${ index } - ${ Date . now ( ) } ` ,
name : suggestion.suggested_name ,
product_type : suggestion.product_type as ProductType ,
category : suggestion.category ,
unit_of_measure : suggestion.unit_of_measure as UnitOfMeasure ,
isSuggested : true ,
confidence_score : suggestion.confidence_score ,
sales_data : suggestion.sales_data ? {
total_quantity : suggestion.sales_data.total_quantity ,
average_daily_sales : suggestion.sales_data.average_daily_sales ,
} : undefined ,
} ) ) ;
setInventoryItems ( items ) ;
}
} , [ initialData ] ) ;
// Filter items
const filteredItems = inventoryItems . filter ( item = > {
if ( activeFilter === 'ingredients' ) return item . product_type === ProductType . INGREDIENT ;
if ( activeFilter === 'finished_products' ) return item . product_type === ProductType . FINISHED_PRODUCT ;
return true ;
} ) ;
// Count by type
const counts = {
all : inventoryItems.length ,
ingredients : inventoryItems.filter ( i = > i . product_type === ProductType . INGREDIENT ) . length ,
finished_products : inventoryItems.filter ( i = > i . product_type === ProductType . FINISHED_PRODUCT ) . length ,
} ;
// Form handlers
const handleAdd = ( ) = > {
setFormData ( {
id : ` manual- ${ Date . now ( ) } ` ,
name : '' ,
product_type : ProductType.INGREDIENT ,
category : '' ,
unit_of_measure : UnitOfMeasure.KILOGRAMS ,
isSuggested : false ,
} ) ;
setEditingId ( null ) ;
setIsAdding ( true ) ;
setFormErrors ( { } ) ;
} ;
const handleEdit = ( item : InventoryItemForm ) = > {
setFormData ( { . . . item } ) ;
setEditingId ( item . id ) ;
setIsAdding ( true ) ;
setFormErrors ( { } ) ;
} ;
const handleDelete = ( id : string ) = > {
setInventoryItems ( items = > items . filter ( item = > item . id !== id ) ) ;
} ;
const validateForm = ( ) : boolean = > {
const errors : Record < string , string > = { } ;
if ( ! formData . name . trim ( ) ) {
errors . name = t ( 'validation:name_required' , 'El nombre es requerido' ) ;
}
if ( ! formData . category ) {
errors . category = t ( 'validation:category_required' , 'La categoría es requerida' ) ;
}
setFormErrors ( errors ) ;
return Object . keys ( errors ) . length === 0 ;
} ;
const handleSave = ( ) = > {
if ( ! validateForm ( ) ) return ;
if ( editingId ) {
// Update existing
setInventoryItems ( items = >
items . map ( item = > ( item . id === editingId ? formData : item ) )
) ;
} else {
// Add new
setInventoryItems ( items = > [ . . . items , formData ] ) ;
}
setIsAdding ( false ) ;
setEditingId ( null ) ;
setFormData ( {
id : '' ,
name : '' ,
product_type : ProductType.INGREDIENT ,
category : '' ,
unit_of_measure : UnitOfMeasure.KILOGRAMS ,
isSuggested : false ,
} ) ;
} ;
const handleCancel = ( ) = > {
setIsAdding ( false ) ;
setEditingId ( null ) ;
setFormErrors ( { } ) ;
} ;
const handleAddTemplate = ( template : IngredientTemplate ) = > {
// Check for duplicates by name
const existingNames = new Set ( inventoryItems . map ( item = > item . name . toLowerCase ( ) ) ) ;
const newItems = template . items
. filter ( item = > ! existingNames . has ( item . name . toLowerCase ( ) ) )
. map ( ( item , index ) = > ( {
id : ` template- ${ template . id } - ${ index } - ${ Date . now ( ) } ` ,
name : item.name ,
product_type : item.product_type ,
category : item.category ,
unit_of_measure : item.unit_of_measure ,
isSuggested : false ,
} ) ) ;
if ( newItems . length > 0 ) {
setInventoryItems ( items = > [ . . . items , . . . newItems ] ) ;
}
} ;
const handleCompleteStep = async ( ) = > {
if ( inventoryItems . length === 0 ) {
setFormErrors ( { submit : t ( 'validation:min_items' , 'Agrega al menos 1 producto para continuar' ) } ) ;
return ;
}
setIsSubmitting ( true ) ;
setFormErrors ( { } ) ;
try {
// STEP 1: Create all inventory items in parallel
// This MUST happen BEFORE sales import because sales records reference inventory IDs
console . log ( '📦 Creating inventory items...' , inventoryItems . length ) ;
console . log ( '📋 Items to create:' , inventoryItems . map ( item = > ( {
name : item.name ,
product_type : item.product_type ,
category : item.category ,
unit_of_measure : item.unit_of_measure
} ) ) ) ;
const createPromises = inventoryItems . map ( ( item , index ) = > {
const ingredientData : IngredientCreate = {
name : item.name ,
product_type : item.product_type ,
category : item.category ,
unit_of_measure : item.unit_of_measure as UnitOfMeasure ,
// All other fields are optional now!
} ;
console . log ( ` 🔄 Creating ingredient ${ index + 1 } / ${ inventoryItems . length } : ` , ingredientData ) ;
return createIngredientMutation . mutateAsync ( {
tenantId ,
ingredientData ,
} ) . catch ( error = > {
console . error ( ` ❌ Failed to create ingredient " ${ item . name } ": ` , error ) ;
console . error ( 'Failed ingredient data:' , ingredientData ) ;
throw error ;
} ) ;
} ) ;
2025-11-13 16:01:08 +01:00
const createdIngredients = await Promise . all ( createPromises ) ;
2025-11-09 09:22:08 +01:00
console . log ( '✅ Inventory items created successfully' ) ;
2025-11-13 16:01:08 +01:00
console . log ( '📋 Created ingredient IDs:' , createdIngredients . map ( ing = > ( { name : ing.name , id : ing.id } ) ) ) ;
2025-11-09 09:22:08 +01:00
// STEP 2: Import sales data (only if file was uploaded)
// Now that inventory exists, sales records can reference the inventory IDs
let salesImported = false ;
if ( initialData ? . uploadedFile && tenantId ) {
try {
console . log ( '📊 Importing sales data from file:' , initialData . uploadedFileName ) ;
await importSalesMutation . mutateAsync ( {
tenantId ,
file : initialData.uploadedFile ,
} ) ;
salesImported = true ;
console . log ( '✅ Sales data imported successfully' ) ;
} catch ( salesError ) {
console . error ( '⚠️ Sales import failed (non-blocking):' , salesError ) ;
// Don't block onboarding if sales import fails
// Inventory is already created, which is the critical part
}
}
2025-11-12 15:34:10 +01:00
// Complete the step with metadata and inventory items
2025-11-13 16:01:08 +01:00
// Map created ingredients to include their real UUIDs
const itemsWithRealIds = createdIngredients . map ( ingredient = > ( {
id : ingredient.id , // Real UUID from the API
name : ingredient.name ,
product_type : ingredient.product_type ,
category : ingredient.category ,
unit_of_measure : ingredient.unit_of_measure ,
} ) ) ;
console . log ( '📦 Passing items with real IDs to next step:' , itemsWithRealIds ) ;
2025-11-09 09:22:08 +01:00
onComplete ( {
2025-11-13 16:01:08 +01:00
inventoryItemsCreated : createdIngredients.length ,
2025-11-09 09:22:08 +01:00
salesDataImported : salesImported ,
2025-11-13 16:01:08 +01:00
inventoryItems : itemsWithRealIds , // Pass the created items with real UUIDs to the next step
2025-11-09 09:22:08 +01:00
} ) ;
} catch ( error ) {
console . error ( 'Error creating inventory items:' , error ) ;
setFormErrors ( { submit : t ( 'error:creating_items' , 'Error al crear los productos. Inténtalo de nuevo.' ) } ) ;
setIsSubmitting ( false ) ;
}
} ;
// Category options based on product type
const getCategoryOptions = ( productType : ProductType ) = > {
if ( productType === ProductType . INGREDIENT ) {
return Object . values ( IngredientCategory ) . map ( cat = > ( {
value : cat ,
label : t ( ` inventory:enums.ingredient_category. ${ cat } ` , cat )
} ) ) ;
} else {
return Object . values ( ProductCategory ) . map ( cat = > ( {
value : cat ,
label : t ( ` inventory:enums.product_category. ${ cat } ` , cat )
} ) ) ;
}
} ;
const unitOptions = Object . values ( UnitOfMeasure ) . map ( unit = > ( {
value : unit ,
label : t ( ` inventory:enums.unit_of_measure. ${ unit } ` , unit )
} ) ) ;
return (
< div className = "space-y-6" >
{ /* Header */ }
< div >
< h2 className = "text-xl md:text-2xl font-bold text-[var(--text-primary)] mb-2" >
{ t ( 'onboarding:inventory_review.title' , 'Revisar Inventario' ) }
< / h2 >
< p className = "text-sm md:text-base text-[var(--text-secondary)]" >
{ t ( 'onboarding:inventory_review.description' , 'Revisa y ajusta los productos detectados. Puedes editar, eliminar o agregar más productos.' ) }
< / p >
< / div >
{ /* 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" >
< AlertCircle className = "w-5 h-5 text-[var(--color-info)]" / >
{ t ( 'setup_wizard:why_this_matters' , '¿Por qué es importante?' ) }
< / h3 >
< p className = "text-sm text-[var(--text-secondary)]" >
{ t ( 'onboarding:inventory_review.why' , 'Estos productos serán la base de tu sistema. Diferenciamos entre Ingredientes (lo que usas para producir) y Productos Terminados (lo que vendes).' ) }
< / p >
< / div >
{ /* Quick Add Templates */ }
< div className = "bg-gradient-to-br from-purple-50 to-blue-50 dark:from-purple-900/10 dark:to-blue-900/10 border border-purple-200 dark:border-purple-700 rounded-lg p-5" >
< div className = "flex items-center gap-2 mb-4" >
< Sparkles className = "w-5 h-5 text-purple-600 dark:text-purple-400" / >
< h3 className = "font-semibold text-[var(--text-primary)]" >
{ t ( 'inventory:templates.title' , 'Plantillas de Ingredientes' ) }
< / h3 >
< / div >
< p className = "text-sm text-[var(--text-secondary)] mb-4" >
{ t ( 'inventory:templates.description' , 'Agrega ingredientes comunes con un solo clic. Solo se agregarán los que no tengas ya.' ) }
< / p >
< div className = "grid grid-cols-1 md:grid-cols-2 gap-3" >
{ INGREDIENT_TEMPLATES . map ( ( template ) = > (
< button
key = { template . id }
onClick = { ( ) = > handleAddTemplate ( template ) }
className = "text-left p-4 bg-white dark:bg-gray-800 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"
>
< div className = "flex items-start gap-3" >
< span className = "text-3xl group-hover:scale-110 transition-transform" > { template . icon } < / span >
< div className = "flex-1 min-w-0" >
< h4 className = "font-medium text-[var(--text-primary)] mb-1" >
{ template . name }
< / h4 >
< p className = "text-xs text-[var(--text-secondary)] mb-2" >
{ template . description }
< / p >
< p className = "text-xs text-purple-600 dark:text-purple-400 font-medium" >
{ template . items . length } { t ( 'inventory:templates.items' , 'ingredientes' ) }
< / p >
< / div >
< / div >
< / button >
) ) }
< / div >
< / div >
{ /* Filter Tabs */ }
< div className = "flex gap-2 border-b border-[var(--border-color)] overflow-x-auto scrollbar-hide -mx-2 px-2" >
< button
onClick = { ( ) = > setActiveFilter ( 'all' ) }
className = { ` px-3 md:px-4 py-3 font-medium transition-colors relative whitespace-nowrap text-sm md:text-base ${
activeFilter === 'all'
? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)] -mb-px'
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
} ` }
>
{ t ( 'inventory:filter.all' , 'Todos' ) } ( { counts . all } )
< / button >
< button
onClick = { ( ) = > setActiveFilter ( 'finished_products' ) }
className = { ` px-3 md:px-4 py-3 font-medium transition-colors relative flex items-center gap-2 whitespace-nowrap text-sm md:text-base ${
activeFilter === 'finished_products'
? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)] -mb-px'
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
} ` }
>
< ShoppingBag className = "w-5 h-5" / >
< span className = "hidden sm:inline" > { t ( 'inventory:filter.finished_products' , 'Productos Terminados' ) } < / span >
< span className = "sm:hidden" > { t ( 'inventory:filter.finished_products_short' , 'Productos' ) } < / span > ( { counts . finished_products } )
< / button >
< button
onClick = { ( ) = > setActiveFilter ( 'ingredients' ) }
className = { ` px-3 md:px-4 py-3 font-medium transition-colors relative flex items-center gap-2 whitespace-nowrap text-sm md:text-base ${
activeFilter === 'ingredients'
? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)] -mb-px'
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
} ` }
>
< Package className = "w-5 h-5" / >
{ t ( 'inventory:filter.ingredients' , 'Ingredientes' ) } ( { counts . ingredients } )
< / button >
< / div >
{ /* Inventory List */ }
< div className = "space-y-3" >
{ filteredItems . length === 0 && (
< div className = "text-center py-8 text-[var(--text-secondary)]" >
{ activeFilter === 'all'
? t ( 'inventory:empty_state' , 'No hay productos. Agrega uno para comenzar.' )
: t ( 'inventory:no_results' , 'No hay productos de este tipo.' ) }
< / div >
) }
{ filteredItems . map ( ( item ) = > (
< React.Fragment key = { item . id } >
{ /* Item Card */ }
< div className = "p-3 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg hover:border-[var(--border-primary)] transition-colors" >
< div className = "flex items-center justify-between" >
< div className = "flex-1 min-w-0" >
< div className = "flex items-center gap-2 mb-1" >
< h5 className = "font-medium text-[var(--text-primary)] truncate" > { item . name } < / h5 >
{ /* Product Type Badge */ }
< span className = { ` text-xs px-2 py-0.5 rounded-full ${
item . product_type === ProductType . FINISHED_PRODUCT
? 'bg-green-100 text-green-800'
: 'bg-blue-100 text-blue-800'
} ` }>
{ item . product_type === ProductType . FINISHED_PRODUCT ? (
< span className = "flex items-center gap-1" >
< ShoppingBag className = "w-3 h-3" / >
{ t ( 'inventory:type.finished_product' , 'Producto' ) }
< / span >
) : (
< span className = "flex items-center gap-1" >
< Package className = "w-3 h-3" / >
{ t ( 'inventory:type.ingredient' , 'Ingrediente' ) }
< / span >
) }
< / span >
{ /* AI Suggested Badge */ }
{ item . isSuggested && item . confidence_score && (
< span className = "px-2 py-0.5 rounded text-xs bg-purple-100 text-purple-800" >
IA { Math . round ( item . confidence_score * 100 ) } %
< / span >
) }
< / div >
< div className = "flex items-center gap-3 mt-1 text-xs text-[var(--text-secondary)]" >
< span > { item . product_type === ProductType . INGREDIENT ? t ( ` inventory:enums.ingredient_category. ${ item . category } ` , item . category ) : t ( ` inventory:enums.product_category. ${ item . category } ` , item . category ) } < / span >
< span > • < / span >
< span > { t ( ` inventory:enums.unit_of_measure. ${ item . unit_of_measure } ` , item . unit_of_measure ) } < / span >
{ item . sales_data && (
< >
< span > • < / span >
< span > { t ( 'inventory:sales_avg' , 'Ventas' ) } : { item . sales_data . average_daily_sales . toFixed ( 1 ) } / día < / span >
< / >
) }
< / div >
< / div >
{ /* Actions */ }
< div className = "flex items-center gap-2 ml-2 md:ml-4" >
< button
onClick = { ( ) = > handleEdit ( item ) }
className = "p-2 md:p-1.5 text-[var(--text-secondary)] hover:text-[var(--color-primary)] hover:bg-[var(--bg-primary)] rounded transition-colors min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
title = { t ( 'common:edit' , 'Editar' ) }
aria - label = { t ( 'common:edit' , 'Editar' ) }
>
< Edit2 className = "w-5 h-5 md:w-4 md:h-4" / >
< / button >
< button
onClick = { ( ) = > handleDelete ( item . id ) }
className = "p-2 md:p-1.5 text-[var(--text-secondary)] hover:text-[var(--color-error)] hover:bg-[var(--color-error)]/10 rounded transition-colors min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
title = { t ( 'common:delete' , 'Eliminar' ) }
aria - label = { t ( 'common:delete' , 'Eliminar' ) }
>
< Trash2 className = "w-5 h-5 md:w-4 md:h-4" / >
< / button >
< / div >
< / div >
< / div >
{ /* Inline Edit Form - appears right below the card being edited */ }
{ editingId === item . id && (
< div className = "border-2 border-[var(--color-primary)] rounded-lg p-3 md:p-4 bg-[var(--bg-secondary)] ml-0 md:ml-4 mt-2" >
< div className = "flex items-center justify-between mb-4" >
< h3 className = "font-medium text-[var(--text-primary)]" >
{ t ( 'inventory:edit_item' , 'Editar Producto' ) }
< / h3 >
< button
type = "button"
onClick = { handleCancel }
className = "text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
>
{ t ( 'common:cancel' , 'Cancelar' ) }
< / button >
< / div >
< div className = "space-y-4" >
{ /* Product Type Selector */ }
< div >
< label className = "block text-sm font-medium text-[var(--text-primary)] mb-3" >
{ t ( 'inventory:product_type' , 'Tipo de Producto' ) } *
< / label >
< div className = "grid grid-cols-1 sm:grid-cols-2 gap-3" >
< button
type = "button"
onClick = { ( ) = > setFormData ( prev = > ( { . . . prev , product_type : ProductType.INGREDIENT , category : '' } ) ) }
className = { ` relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${
formData . product_type === ProductType . INGREDIENT
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
: 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
} ` }
>
{ formData . product_type === ProductType . INGREDIENT && (
< CheckCircle2 className = "absolute top-2 right-2 w-5 h-5 text-[var(--color-primary)]" / >
) }
< Package className = "w-6 h-6 mb-2 text-[var(--color-primary)]" / >
< div className = "font-medium text-[var(--text-primary)]" >
{ t ( 'inventory:type.ingredient' , 'Ingrediente' ) }
< / div >
< div className = "text-xs text-[var(--text-secondary)] mt-1" >
{ t ( 'inventory:type.ingredient_desc' , 'Materias primas para producir' ) }
< / div >
< / button >
< button
type = "button"
onClick = { ( ) = > setFormData ( prev = > ( { . . . prev , product_type : ProductType.FINISHED_PRODUCT , category : '' } ) ) }
className = { ` relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${
formData . product_type === ProductType . FINISHED_PRODUCT
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
: 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
} ` }
>
{ formData . product_type === ProductType . FINISHED_PRODUCT && (
< CheckCircle2 className = "absolute top-2 right-2 w-5 h-5 text-[var(--color-primary)]" / >
) }
< ShoppingBag className = "w-6 h-6 mb-2 text-[var(--color-primary)]" / >
< div className = "font-medium text-[var(--text-primary)]" >
{ t ( 'inventory:type.finished_product' , 'Producto Terminado' ) }
< / div >
< div className = "text-xs text-[var(--text-secondary)] mt-1" >
{ t ( 'inventory:type.finished_product_desc' , 'Productos que vendes' ) }
< / div >
< / button >
< / div >
< / div >
{ /* Name */ }
< div >
< label className = "block text-sm font-medium text-[var(--text-primary)] mb-1" >
{ t ( 'inventory:name' , 'Nombre' ) } *
< / label >
< input
type = "text"
value = { formData . name }
onChange = { ( e ) = > setFormData ( prev = > ( { . . . prev , name : e.target.value } ) ) }
className = "w-full px-3 py-2 border border-[var(--border-color)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
placeholder = { t ( 'inventory:name_placeholder' , 'Ej: Harina de trigo' ) }
/ >
{ formErrors . name && (
< p className = "text-xs text-[var(--color-danger)] mt-1" > { formErrors . name } < / p >
) }
< / div >
{ /* Category */ }
< div >
< label className = "block text-sm font-medium text-[var(--text-primary)] mb-1" >
{ t ( 'inventory:category' , 'Categoría' ) } *
< / label >
< select
value = { formData . category }
onChange = { ( e ) = > setFormData ( prev = > ( { . . . prev , category : e.target.value } ) ) }
className = "w-full px-3 py-2 border border-[var(--border-color)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
>
< option value = "" > { t ( 'common:select' , 'Seleccionar...' ) } < / option >
{ getCategoryOptions ( formData . product_type ) . map ( opt = > (
< option key = { opt . value } value = { opt . value } > { opt . label } < / option >
) ) }
< / select >
{ formErrors . category && (
< p className = "text-xs text-[var(--color-danger)] mt-1" > { formErrors . category } < / p >
) }
< / div >
{ /* Unit of Measure */ }
< div >
< label className = "block text-sm font-medium text-[var(--text-primary)] mb-1" >
{ t ( 'inventory:unit_of_measure' , 'Unidad de Medida' ) } *
< / label >
< select
value = { formData . unit_of_measure }
onChange = { ( e ) = > setFormData ( prev = > ( { . . . prev , unit_of_measure : e.target.value as UnitOfMeasure } ) ) }
className = "w-full px-3 py-2 border border-[var(--border-color)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
>
{ unitOptions . map ( opt = > (
< option key = { opt . value } value = { opt . value } > { opt . label } < / option >
) ) }
< / select >
< / div >
{ /* Form Actions */ }
< div className = "flex justify-end pt-2" >
< Button onClick = { handleSave } >
< CheckCircle2 className = "w-4 h-4 mr-2" / >
{ t ( 'common:save' , 'Guardar' ) }
< / Button >
< / div >
< / div >
< / div >
) }
< / React.Fragment >
) ) }
< / div >
{ /* Add Button - hidden when adding or editing */ }
{ ! isAdding && ! editingId && (
< button
onClick = { handleAdd }
className = "w-full border-2 border-dashed border-[var(--color-primary)]/30 rounded-lg p-4 md:p-4 hover:border-[var(--color-primary)]/50 transition-colors flex items-center justify-center gap-2 text-[var(--color-primary)] min-h-[44px] font-medium"
>
< Plus className = "w-5 h-5" / >
< span className = "text-sm md:text-base" > { t ( 'inventory:add_item' , 'Agregar Producto' ) } < / span >
< / button >
) }
{ /* Add New Item Form - only shown when adding (not editing) */ }
{ isAdding && ! editingId && (
< div className = "border-2 border-[var(--color-primary)] rounded-lg p-3 md:p-4 bg-[var(--bg-secondary)]" >
< div className = "flex items-center justify-between mb-4" >
< h3 className = "font-medium text-[var(--text-primary)]" >
{ t ( 'inventory:add_item' , 'Agregar Producto' ) }
< / h3 >
< button
type = "button"
onClick = { handleCancel }
className = "text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
>
{ t ( 'common:cancel' , 'Cancelar' ) }
< / button >
< / div >
< div className = "space-y-4" >
{ /* Product Type Selector */ }
< div >
< label className = "block text-sm font-medium text-[var(--text-primary)] mb-3" >
{ t ( 'inventory:product_type' , 'Tipo de Producto' ) } *
< / label >
< div className = "grid grid-cols-1 sm:grid-cols-2 gap-3" >
< button
type = "button"
onClick = { ( ) = > setFormData ( prev = > ( { . . . prev , product_type : ProductType.INGREDIENT , category : '' } ) ) }
className = { ` relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${
formData . product_type === ProductType . INGREDIENT
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
: 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
} ` }
>
{ formData . product_type === ProductType . INGREDIENT && (
< CheckCircle2 className = "absolute top-2 right-2 w-5 h-5 text-[var(--color-primary)]" / >
) }
< Package className = "w-6 h-6 mb-2 text-[var(--color-primary)]" / >
< div className = "font-medium text-[var(--text-primary)]" >
{ t ( 'inventory:type.ingredient' , 'Ingrediente' ) }
< / div >
< div className = "text-xs text-[var(--text-secondary)] mt-1" >
{ t ( 'inventory:type.ingredient_desc' , 'Materias primas para producir' ) }
< / div >
< / button >
< button
type = "button"
onClick = { ( ) = > setFormData ( prev = > ( { . . . prev , product_type : ProductType.FINISHED_PRODUCT , category : '' } ) ) }
className = { ` relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${
formData . product_type === ProductType . FINISHED_PRODUCT
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
: 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
} ` }
>
{ formData . product_type === ProductType . FINISHED_PRODUCT && (
< CheckCircle2 className = "absolute top-2 right-2 w-5 h-5 text-[var(--color-primary)]" / >
) }
< ShoppingBag className = "w-6 h-6 mb-2 text-[var(--color-primary)]" / >
< div className = "font-medium text-[var(--text-primary)]" >
{ t ( 'inventory:type.finished_product' , 'Producto Terminado' ) }
< / div >
< div className = "text-xs text-[var(--text-secondary)] mt-1" >
{ t ( 'inventory:type.finished_product_desc' , 'Productos que vendes' ) }
< / div >
< / button >
< / div >
< / div >
{ /* Name */ }
< div >
< label className = "block text-sm font-medium text-[var(--text-primary)] mb-1" >
{ t ( 'inventory:name' , 'Nombre' ) } *
< / label >
< input
type = "text"
value = { formData . name }
onChange = { ( e ) = > setFormData ( prev = > ( { . . . prev , name : e.target.value } ) ) }
className = "w-full px-3 py-2 border border-[var(--border-color)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
placeholder = { t ( 'inventory:name_placeholder' , 'Ej: Harina de trigo' ) }
/ >
{ formErrors . name && (
< p className = "text-xs text-[var(--color-danger)] mt-1" > { formErrors . name } < / p >
) }
< / div >
{ /* Category */ }
< div >
< label className = "block text-sm font-medium text-[var(--text-primary)] mb-1" >
{ t ( 'inventory:category' , 'Categoría' ) } *
< / label >
< select
value = { formData . category }
onChange = { ( e ) = > setFormData ( prev = > ( { . . . prev , category : e.target.value } ) ) }
className = "w-full px-3 py-2 border border-[var(--border-color)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
>
< option value = "" > { t ( 'common:select' , 'Seleccionar...' ) } < / option >
{ getCategoryOptions ( formData . product_type ) . map ( opt = > (
< option key = { opt . value } value = { opt . value } > { opt . label } < / option >
) ) }
< / select >
{ formErrors . category && (
< p className = "text-xs text-[var(--color-danger)] mt-1" > { formErrors . category } < / p >
) }
< / div >
{ /* Unit of Measure */ }
< div >
< label className = "block text-sm font-medium text-[var(--text-primary)] mb-1" >
{ t ( 'inventory:unit_of_measure' , 'Unidad de Medida' ) } *
< / label >
< select
value = { formData . unit_of_measure }
onChange = { ( e ) = > setFormData ( prev = > ( { . . . prev , unit_of_measure : e.target.value as UnitOfMeasure } ) ) }
className = "w-full px-3 py-2 border border-[var(--border-color)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
>
{ unitOptions . map ( opt = > (
< option key = { opt . value } value = { opt . value } > { opt . label } < / option >
) ) }
< / select >
< / div >
{ /* Form Actions */ }
< div className = "flex justify-end pt-2" >
< Button onClick = { handleSave } >
< CheckCircle2 className = "w-4 h-4 mr-2" / >
{ t ( 'common:add' , 'Agregar' ) }
< / Button >
< / div >
< / div >
< / div >
) }
{ /* Submit Error */ }
{ formErrors . submit && (
< div className = "bg-[var(--color-danger)]/10 border border-[var(--color-danger)]/20 rounded-lg p-4" >
< p className = "text-sm text-[var(--color-danger)]" > { formErrors . submit } < / p >
< / div >
) }
{ /* Summary */ }
< div className = "bg-[var(--bg-secondary)] rounded-lg p-4" >
< p className = "text-sm text-[var(--text-secondary)]" >
{ t ( 'inventory:summary' , 'Resumen' ) } : { counts . finished_products } { t ( 'inventory:finished_products' , 'productos terminados' ) } , { counts . ingredients } { t ( 'inventory:ingredients_count' , 'ingredientes' ) }
< / p >
< / div >
{ /* Navigation */ }
< div className = "flex flex-col-reverse sm:flex-row justify-between gap-3 sm:gap-4 pt-6 border-t" >
< Button
variant = "outline"
onClick = { onPrevious }
disabled = { isSubmitting || isFirstStep }
className = "w-full sm:w-auto"
>
< span className = "hidden sm:inline" > { t ( 'common:back' , '← Atrás' ) } < / span >
< span className = "sm:hidden" > { t ( 'common:back' , 'Atrás' ) } < / span >
< / Button >
< Button
onClick = { handleCompleteStep }
disabled = { inventoryItems . length === 0 || isSubmitting }
className = "w-full sm:w-auto sm:min-w-[200px]"
>
{ isSubmitting
? t ( 'common:saving' , 'Guardando...' )
: < >
< span className = "hidden md:inline" > { t ( 'common:continue' , 'Continuar' ) } ( { inventoryItems . length } { t ( 'common:items' , 'productos' ) } ) → < / span >
< span className = "md:hidden" > { t ( 'common:continue' , 'Continuar' ) } ( { inventoryItems . length } ) → < / span >
< / >
}
< / Button >
< / div >
< / div >
) ;
} ;