diff --git a/REDESIGN_SUMMARY.md b/REDESIGN_SUMMARY.md new file mode 100644 index 00000000..5113d86f --- /dev/null +++ b/REDESIGN_SUMMARY.md @@ -0,0 +1,188 @@ +# AI Inventory Step Redesign - Key Differences + +## Overview +Completely redesigned UploadSalesDataStep to follow the suppliers/recipes pattern with list-based management and deferred creation. + +## Major Changes + +### 1. **Data Model** +**BEFORE**: +```typescript +interface InventoryItem { + suggestion_id: string; + selected: boolean; // Checkbox selection + stock_quantity: number; + cost_per_unit: number; +} +``` + +**AFTER**: +```typescript +interface InventoryItemForm { + id: string; // UI tracking + name: string; + // ... all inventory fields + isSuggested: boolean; // Track if from AI or manual + // NO "selected" field - all items in list will be created +} +``` + +### 2. **Creation Timing** +**BEFORE**: +- Checkbox selection UI +- "Create Inventory" button → Creates immediately → Proceeds to next step + +**AFTER**: +- List-based UI (like suppliers/recipes) +- Items added to list (NOT created yet) +- "Next" button → Creates ALL items → Proceeds to next step + +### 3. **UI Pattern** + +**BEFORE** (Old Pattern): +``` +┌─────────────────────────────────────┐ +│ ☑️ Product 1 [Edit fields inline] │ +│ ☐ Product 2 [Edit fields inline] │ +│ ☑️ Product 3 [Edit fields inline] │ +│ │ +│ [Create 2 Selected Items] │ +└─────────────────────────────────────┘ +``` + +**AFTER** (New Pattern - Like Suppliers/Recipes): +``` +┌─────────────────────────────────────┐ +│ Product 1 (AI 95%) [Edit][Delete]│ +│ Stock: 50kg Cost: €5.00 │ +│ │ +│ Product 2 (Manual) [Edit][Delete]│ +│ Stock: 30kg Cost: €3.00 │ +│ │ +│ [➕ Add Ingredient Manually] │ +│ │ +│ ────────────────────────────────── │ +│ 3 ingredients - Ready! [Next →] │ +└─────────────────────────────────────┘ +``` + +### 4. **Manual Addition** +**BEFORE**: No way to add manual ingredients + +**AFTER**: +- "Add Ingredient Manually" button +- Full form with all fields +- Adds to the same list as AI suggestions +- Can edit/delete both AI and manual items + +### 5. **Edit/Delete** +**BEFORE**: +- Inline editing only +- No delete (just deselect) + +**AFTER**: +- Click "Edit" → Opens form with all fields +- Click "Delete" → Removes from list +- Works for both AI suggestions and manual entries + +### 6. **Code Structure** + +**BEFORE** (Old): +```typescript +// State +const [inventoryItems, setInventoryItems] = useState([]); + +// Selection toggle +const handleToggleSelection = (id: string) => { + setInventoryItems(items => + items.map(item => + item.suggestion_id === id ? { ...item, selected: !item.selected } : item + ) + ); +}; + +// Create button - immediate creation +const handleCreateInventory = async () => { + const selectedItems = inventoryItems.filter(item => item.selected); + // Create immediately... + await Promise.all(creationPromises); + onComplete(); // Then proceed +}; +``` + +**AFTER** (New): +```typescript +// State +const [inventoryItems, setInventoryItems] = useState([]); +const [isAdding, setIsAdding] = useState(false); +const [editingId, setEditingId] = useState(null); +const [formData, setFormData] = useState({ ... }); + +// Add/Edit item in list (NOT in database) +const handleSubmitForm = (e: React.FormEvent) => { + if (editingId) { + // Update in list + setInventoryItems(items => items.map(item => + item.id === editingId ? { ...formData, id: editingId } : item + )); + } else { + // Add to list + setInventoryItems(items => [...items, newItem]); + } + resetForm(); +}; + +// Delete from list +const handleDelete = (itemId: string) => { + setInventoryItems(items => items.filter(item => item.id !== itemId)); +}; + +// Next button - create ALL at once +const handleNext = async () => { + // Create ALL items in the list + const creationPromises = inventoryItems.map(item => createIngredient.mutateAsync(...)); + await Promise.allSettled(creationPromises); + onComplete(); // Then proceed +}; +``` + +### 7. **Component Sections** + +**BEFORE**: 2 main sections +1. File upload view +2. Checkbox selection + edit view + +**AFTER**: 2 main sections +1. File upload view (same) +2. **List management view**: + - "Why This Matters" info box + - Ingredient list (cards with edit/delete) + - Add/Edit form (appears on click) + - Navigation with "Next" button + +## Key Benefits + +✅ **Consistent UI**: Matches suppliers/recipes pattern exactly +✅ **Flexibility**: Users can review, edit, delete, and add items before creating +✅ **Deferred Creation**: All items created at once when clicking "Next" +✅ **Manual Addition**: Users can add ingredients beyond AI suggestions +✅ **Better UX**: Clear "Edit" and "Delete" actions per item +✅ **Unified Pattern**: Same workflow for AI and manual items + +## Files Changed + +- `/home/user/bakery_ia/frontend/src/components/domain/onboarding/steps/UploadSalesDataStep.tsx` - **Completely rewritten** (963 lines) +- `/home/user/bakery_ia/frontend/src/components/domain/setup-wizard/steps/RecipesSetupStep.tsx` - Added Next button (2 lines) + +## Testing Checklist + +- [ ] Upload sales data file → AI suggestions load correctly +- [ ] Edit AI suggestion → Changes saved to list +- [ ] Delete AI suggestion → Removed from list +- [ ] Add manual ingredient → Added to list +- [ ] Edit manual ingredient → Changes saved +- [ ] Delete manual ingredient → Removed from list +- [ ] Click "Next" with 0 items → Error shown +- [ ] Click "Next" with items → All created and proceeds to next step +- [ ] Form validation works (required fields, min values) +- [ ] UI matches suppliers/recipes styling diff --git a/frontend/src/components/domain/onboarding/steps/UploadSalesDataStep.tsx b/frontend/src/components/domain/onboarding/steps/UploadSalesDataStep.tsx index 3f124826..236e6b25 100644 --- a/frontend/src/components/domain/onboarding/steps/UploadSalesDataStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/UploadSalesDataStep.tsx @@ -1,7 +1,6 @@ import React, { useState, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '../../../ui/Button'; -import { Input } from '../../../ui/Input'; import { useCurrentTenant } from '../../../../stores/tenant.store'; import { useCreateIngredient, useClassifyBatch } from '../../../../api/hooks/inventory'; import { useValidateImportFile, useImportSalesData } from '../../../../api/hooks/sales'; @@ -23,38 +22,31 @@ interface ProgressState { message: string; } -interface InventoryItem { +interface InventoryItemForm { id: string; // Unique ID for UI tracking - suggestion_id?: string; // Only for AI suggestions - original_name?: string; // Only for AI suggestions - suggested_name: string; // The actual ingredient name + name: string; product_type: string; category: string; unit_of_measure: string; - confidence_score?: number; // Only for AI suggestions + stock_quantity: number; + cost_per_unit: number; estimated_shelf_life_days: number; requires_refrigeration: boolean; requires_freezing: boolean; is_seasonal: boolean; - suggested_supplier?: string; - notes?: string; + low_stock_threshold: number; + reorder_point: number; + notes: string; + // AI suggestion metadata (if from AI) + isSuggested: boolean; + confidence_score?: number; sales_data?: { total_quantity: number; average_daily_sales: number; - peak_day: string; - frequency: number; }; - // UI-specific fields - isSuggested: boolean; // true for AI, false for manual - isExpanded: boolean; // for expand/collapse UI - stock_quantity: number; - cost_per_unit: number; - low_stock_threshold: number; - reorder_point: number; } export const UploadSalesDataStep: React.FC = ({ - onPrevious, onComplete, isFirstStep }) => { @@ -62,14 +54,35 @@ export const UploadSalesDataStep: React.FC = ({ const [selectedFile, setSelectedFile] = useState(null); const [isValidating, setIsValidating] = useState(false); const [validationResult, setValidationResult] = useState(null); - const [inventoryItems, setInventoryItems] = useState([]); + const [inventoryItems, setInventoryItems] = useState([]); const [showInventoryStep, setShowInventoryStep] = useState(false); - const [isCreating, setIsCreating] = useState(false); const [error, setError] = useState(''); const [progressState, setProgressState] = useState(null); const [showGuide, setShowGuide] = useState(false); const fileInputRef = useRef(null); + // Form state for adding/editing + const [isAdding, setIsAdding] = useState(false); + const [editingId, setEditingId] = useState(null); + const [formData, setFormData] = useState({ + id: '', + name: '', + product_type: 'ingredient', + category: '', + unit_of_measure: 'kg', + stock_quantity: 0, + cost_per_unit: 0, + estimated_shelf_life_days: 30, + requires_refrigeration: false, + requires_freezing: false, + is_seasonal: false, + low_stock_threshold: 0, + reorder_point: 0, + notes: '', + isSuggested: false, + }); + const [formErrors, setFormErrors] = useState>({}); + const currentTenant = useCurrentTenant(); const { user } = useAuth(); const validateFileMutation = useValidateImportFile(); @@ -83,8 +96,6 @@ export const UploadSalesDataStep: React.FC = ({ setSelectedFile(file); setValidationResult(null); setError(''); - - // Automatically trigger validation and classification await handleAutoValidateAndClassify(file); } }; @@ -96,8 +107,6 @@ export const UploadSalesDataStep: React.FC = ({ setSelectedFile(file); setValidationResult(null); setError(''); - - // Automatically trigger validation and classification await handleAutoValidateAndClassify(file); } }; @@ -120,12 +129,9 @@ export const UploadSalesDataStep: React.FC = ({ file }); - // The API returns the validation result directly (not wrapped) if (validationResult && validationResult.is_valid !== undefined) { setValidationResult(validationResult); setProgressState({ stage: 'analyzing', progress: 60, message: 'Validación exitosa. Generando sugerencias automáticamente...' }); - - // Step 2: Automatically trigger classification await generateInventorySuggestionsAuto(validationResult); } else { setError('Respuesta de validación inválida del servidor'); @@ -139,7 +145,6 @@ export const UploadSalesDataStep: React.FC = ({ } }; - const generateInventorySuggestionsAuto = async (validationData: ImportValidationResponse) => { if (!currentTenant?.id) { setError('No hay datos de validación disponibles para generar sugerencias'); @@ -151,7 +156,6 @@ export const UploadSalesDataStep: React.FC = ({ try { setProgressState({ stage: 'analyzing', progress: 65, message: 'Analizando productos de ventas...' }); - // Extract product data from validation result - use the exact backend structure const products = validationData.product_list?.map((productName: string) => ({ product_name: productName })) || []; @@ -165,7 +169,6 @@ export const UploadSalesDataStep: React.FC = ({ setProgressState({ stage: 'classifying', progress: 75, message: 'Clasificando productos con IA...' }); - // Call the classification API const classificationResponse = await classifyBatchMutation.mutateAsync({ tenantId: currentTenant.id, products @@ -173,49 +176,38 @@ export const UploadSalesDataStep: React.FC = ({ setProgressState({ stage: 'preparing', progress: 90, message: 'Preparando sugerencias de inventario...' }); - // Convert API response to InventoryItem format - use exact backend structure plus UI fields - const items: InventoryItem[] = classificationResponse.suggestions.map((suggestion: ProductSuggestionResponse, index: number) => { - // Calculate default stock quantity based on sales data + // Convert AI suggestions to inventory items (NOT created yet, just added to list) + const items: InventoryItemForm[] = classificationResponse.suggestions.map((suggestion: ProductSuggestionResponse, index: number) => { const defaultStock = Math.max( - Math.ceil((suggestion.sales_data?.average_daily_sales || 1) * 7), // 1 week supply + Math.ceil((suggestion.sales_data?.average_daily_sales || 1) * 7), 1 ); - - // Estimate cost per unit based on category const estimatedCost = suggestion.category === 'Dairy' ? 5.0 : - suggestion.category === 'Baking Ingredients' ? 2.0 : - 3.0; - - // Calculate inventory management defaults + suggestion.category === 'Baking Ingredients' ? 2.0 : 3.0; const minimumStock = Math.max(1, Math.ceil(defaultStock * 0.2)); - const calculatedReorderPoint = Math.ceil(defaultStock * 0.3); - const reorderPoint = Math.max(minimumStock + 2, calculatedReorderPoint, minimumStock + 1); + const reorderPoint = Math.max(minimumStock + 2, Math.ceil(defaultStock * 0.3), minimumStock + 1); return { - // UI tracking id: `ai-${index}-${Date.now()}`, - // AI suggestion fields - suggestion_id: suggestion.suggestion_id, - original_name: suggestion.original_name, - suggested_name: suggestion.suggested_name, + name: suggestion.suggested_name, product_type: suggestion.product_type, category: suggestion.category, unit_of_measure: suggestion.unit_of_measure, - confidence_score: suggestion.confidence_score, + stock_quantity: defaultStock, + cost_per_unit: estimatedCost, estimated_shelf_life_days: suggestion.estimated_shelf_life_days || 30, requires_refrigeration: suggestion.requires_refrigeration, requires_freezing: suggestion.requires_freezing, is_seasonal: suggestion.is_seasonal, - suggested_supplier: suggestion.suggested_supplier, - notes: suggestion.notes, - sales_data: suggestion.sales_data, - // UI-specific fields - isSuggested: true, - isExpanded: false, // Start collapsed - stock_quantity: defaultStock, - cost_per_unit: estimatedCost, low_stock_threshold: minimumStock, - reorder_point: reorderPoint + reorder_point: reorderPoint, + notes: `AI generado - Confianza: ${Math.round(suggestion.confidence_score * 100)}%`, + 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, }; }); @@ -231,35 +223,96 @@ export const UploadSalesDataStep: React.FC = ({ } }; + // Form validation + const validateForm = (): boolean => { + const newErrors: Record = {}; - const handleToggleSelection = (id: string) => { - setInventoryItems(items => - items.map(item => - item.suggestion_id === id ? { ...item, selected: !item.selected } : item - ) - ); + if (!formData.name.trim()) { + newErrors.name = 'El nombre es requerido'; + } + if (!formData.category.trim()) { + newErrors.category = 'La categoría es requerida'; + } + if (formData.stock_quantity < 0) { + newErrors.stock_quantity = 'El stock debe ser 0 o mayor'; + } + if (formData.cost_per_unit < 0) { + newErrors.cost_per_unit = 'El costo debe ser 0 o mayor'; + } + if (formData.estimated_shelf_life_days <= 0) { + newErrors.estimated_shelf_life_days = 'Los días de caducidad deben ser mayores a 0'; + } + + setFormErrors(newErrors); + return Object.keys(newErrors).length === 0; }; - const handleUpdateItem = (id: string, field: keyof InventoryItem, value: number) => { - setInventoryItems(items => - items.map(item => - item.suggestion_id === id ? { ...item, [field]: value } : item - ) - ); + // Add or update item in list + const handleSubmitForm = (e: React.FormEvent) => { + e.preventDefault(); + if (!validateForm()) return; + + if (editingId) { + // Update existing item + setInventoryItems(items => + items.map(item => + item.id === editingId ? { ...formData, id: editingId } : item + ) + ); + setEditingId(null); + } else { + // Add new item + const newItem: InventoryItemForm = { + ...formData, + id: `manual-${Date.now()}`, + isSuggested: false, + }; + setInventoryItems(items => [...items, newItem]); + } + + resetForm(); }; - const handleSelectAll = () => { - const allSelected = inventoryItems.every(item => item.selected); - setInventoryItems(items => - items.map(item => ({ ...item, selected: !allSelected })) - ); + const resetForm = () => { + setFormData({ + id: '', + name: '', + product_type: 'ingredient', + category: '', + unit_of_measure: 'kg', + stock_quantity: 0, + cost_per_unit: 0, + estimated_shelf_life_days: 30, + requires_refrigeration: false, + requires_freezing: false, + is_seasonal: false, + low_stock_threshold: 0, + reorder_point: 0, + notes: '', + isSuggested: false, + }); + setFormErrors({}); + setIsAdding(false); + setEditingId(null); }; - const handleCreateInventory = async () => { - const selectedItems = inventoryItems.filter(item => item.selected); + const handleEdit = (item: InventoryItemForm) => { + setFormData(item); + setEditingId(item.id); + setIsAdding(true); + }; - if (selectedItems.length === 0) { - setError('Por favor selecciona al menos un artículo de inventario para crear'); + const handleDelete = (itemId: string) => { + if (!window.confirm('¿Estás seguro de que quieres eliminar este ingrediente de la lista?')) { + return; + } + setInventoryItems(items => items.filter(item => item.id !== itemId)); + }; + + // Create all inventory items when Next is clicked + const handleNext = async () => { + if (inventoryItems.length === 0) { + setError('Por favor agrega al menos un ingrediente antes de continuar'); return; } @@ -268,36 +321,29 @@ export const UploadSalesDataStep: React.FC = ({ return; } - setIsCreating(true); - setError(''); + setProgressState({ + stage: 'creating_inventory', + progress: 10, + message: `Creando ${inventoryItems.length} ingredientes...` + }); try { - // Parallel inventory creation - setProgressState({ - stage: 'creating_inventory', - progress: 10, - message: `Creando ${selectedItems.length} artículos de inventario...` - }); - - const creationPromises = selectedItems.map(item => { - 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); - + // Create all ingredients in parallel + const creationPromises = inventoryItems.map(item => { const ingredientData = { - name: item.suggested_name, + name: item.name, product_type: item.product_type, category: item.category, unit_of_measure: item.unit_of_measure, - low_stock_threshold: minimumStock, + low_stock_threshold: item.low_stock_threshold, max_stock_level: item.stock_quantity * 2, - reorder_point: reorderPoint, - shelf_life_days: item.estimated_shelf_life_days || 30, + reorder_point: item.reorder_point, + shelf_life_days: item.estimated_shelf_life_days, requires_refrigeration: item.requires_refrigeration, requires_freezing: item.requires_freezing, is_seasonal: item.is_seasonal, average_cost: item.cost_per_unit, - notes: item.notes || `Creado durante onboarding - Confianza: ${Math.round(item.confidence_score * 100)}%` + notes: item.notes || undefined, }; return createIngredient.mutateAsync({ @@ -318,19 +364,18 @@ export const UploadSalesDataStep: React.FC = ({ const failedCount = results.filter(r => r.status === 'rejected').length; if (failedCount > 0) { - console.warn(`${failedCount} items failed to create out of ${selectedItems.length}`); + console.warn(`${failedCount} ingredientes fallaron al crear de ${inventoryItems.length}`); } - console.log(`Successfully created ${createdIngredients.length} inventory items in parallel`); + console.log(`Creados exitosamente ${createdIngredients.length} ingredientes`); - // After inventory creation, import the sales data + // Import sales data if available setProgressState({ stage: 'importing_sales', progress: 50, message: 'Importando datos de ventas...' }); - console.log('Importing sales data after inventory creation...'); let salesImportResult = null; try { if (selectedFile) { @@ -338,24 +383,18 @@ export const UploadSalesDataStep: React.FC = ({ tenantId: currentTenant.id, file: selectedFile }); - salesImportResult = result; if (result.success) { - console.log('Sales data imported successfully'); - setProgressState({ - stage: 'completing', - progress: 95, - message: 'Finalizando configuración...' - }); - } else { - console.warn('Sales import completed with issues:', result.error); + console.log('Datos de ventas importados exitosamente'); } } } catch (importError) { - console.error('Error importing sales data:', importError); + console.error('Error importando datos de ventas:', importError); } setProgressState(null); + + // Complete step onComplete({ createdIngredients, totalItems: createdIngredients.length, @@ -367,9 +406,8 @@ export const UploadSalesDataStep: React.FC = ({ userId: user?.id }); } 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); + console.error('Error creando ingredientes:', err); + setError('Error al crear ingredientes. Por favor, inténtalo de nuevo.'); setProgressState(null); } }; @@ -382,10 +420,23 @@ export const UploadSalesDataStep: React.FC = ({ 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); + const categoryOptions = [ + 'Baking Ingredients', + 'Dairy', + 'Fruits', + 'Vegetables', + 'Meat', + 'Seafood', + 'Spices', + 'Other' + ]; + const unitOptions = ['kg', 'g', 'L', 'ml', 'units', 'dozen']; + + // INVENTORY LIST VIEW (after AI suggestions loaded) if (showInventoryStep) { + const canContinue = inventoryItems.length >= 1; + return (
{/* Why This Matters */} @@ -394,214 +445,295 @@ export const UploadSalesDataStep: React.FC = ({ - {t('onboarding:ai_suggestions.why_title', 'AI Smart Inventory')} + {t('onboarding:ai_suggestions.why_title', 'Configurar Inventario')}

- {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.')} + Revisa y edita los ingredientes sugeridos por IA. Puedes agregar más ingredientes manualmente. Cuando hagas clic en "Siguiente", se crearán todos los ingredientes.

- {/* Progress indicator */} -
-
- - - - - {selectedCount} de {inventoryItems.length} {t('onboarding:ai_suggestions.items_selected', 'artículos seleccionados')} - -
-
- {selectedCount >= 1 && ( -
- - - - {t('onboarding:ai_suggestions.minimum_met', 'Mínimo alcanzado')} -
- )} - -
-
- - {/* Product suggestions grid */} + {/* Inventory Items List */}

- {t('onboarding:ai_suggestions.suggested_products', 'Productos Sugeridos')} + Ingredientes ({inventoryItems.length})

-
- {inventoryItems.map((item) => ( -
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)]' - }`} - > -
- {/* Checkbox */} -
-
- {item.selected && ( - - - - )} -
-
- {/* Product info */} -
-
- {item.suggested_name} -
-

- {item.category} • {item.unit_of_measure} -

- - {/* Tags */} -
- - {Math.round(item.confidence_score * 100)}% confianza - - {item.requires_refrigeration && ( - - ❄️ Refrigeración - - )} - {item.requires_freezing && ( - - 🧊 Congelación - - )} - {item.is_seasonal && ( - - 🌿 Estacional - - )} -
- - {/* Sales data preview */} - {item.sales_data && ( -
-
- - 📊 {item.sales_data.average_daily_sales.toFixed(1)}/día - - - 📦 {item.sales_data.total_quantity} total - -
-
- )} -
-
-
- ))} -
-
- - {/* Edit selected items section */} - {selectedCount > 0 && ( -
-
-
-

- - - - {t('onboarding:ai_suggestions.edit_details', 'Configurar Detalles')} -

-

- {t('onboarding:ai_suggestions.edit_desc', 'Ajusta el stock inicial y costos para los artículos seleccionados')} -

-
-
- -
- {inventoryItems.filter(item => item.selected).map((item) => ( + {inventoryItems.length > 0 ? ( +
+ {inventoryItems.map((item) => (
-
- {item.suggested_name} -
-
-
- - handleUpdateItem( - item.suggestion_id, - 'stock_quantity', - Number(e.target.value) +
+
+
+
+ {item.name} +
+ {item.isSuggested && item.confidence_score && ( + + IA {Math.round(item.confidence_score * 100)}% + )} - 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" - /> +
+

+ {item.category} • {item.unit_of_measure} +

+
+ Stock: {item.stock_quantity} {item.unit_of_measure} + Costo: €{item.cost_per_unit.toFixed(2)}/{item.unit_of_measure} + Caducidad: {item.estimated_shelf_life_days} días +
+ {item.sales_data && ( +
+ 📊 Ventas: {item.sales_data.average_daily_sales.toFixed(1)}/día +
+ )}
-
- - handleUpdateItem( - item.suggestion_id, - 'cost_per_unit', - Number(e.target.value) - )} - 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" - /> -
-
- - handleUpdateItem( - item.suggestion_id, - 'estimated_shelf_life_days', - Number(e.target.value) - )} - 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" - /> +
+ +
))}
+ ) : ( +
+

+ No hay ingredientes en la lista todavía +

+
+ )} +
+ + {/* Add/Edit Form */} + {isAdding ? ( +
+

+ {editingId ? 'Editar Ingrediente' : 'Agregar Ingrediente Manualmente'} +

+
+
+
+ + setFormData({ ...formData, name: e.target.value })} + className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" + placeholder="Ej: Harina de trigo" + /> + {formErrors.name && ( +

{formErrors.name}

+ )} +
+ +
+ + + {formErrors.category && ( +

{formErrors.category}

+ )} +
+ +
+ + +
+ +
+ + setFormData({ ...formData, stock_quantity: parseFloat(e.target.value) || 0 })} + className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" + /> + {formErrors.stock_quantity && ( +

{formErrors.stock_quantity}

+ )} +
+ +
+ + setFormData({ ...formData, cost_per_unit: parseFloat(e.target.value) || 0 })} + className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" + /> + {formErrors.cost_per_unit && ( +

{formErrors.cost_per_unit}

+ )} +
+ +
+ + setFormData({ ...formData, estimated_shelf_life_days: parseInt(e.target.value) || 30 })} + className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" + /> + {formErrors.estimated_shelf_life_days && ( +

{formErrors.estimated_shelf_life_days}

+ )} +
+ +
+ + setFormData({ ...formData, low_stock_threshold: parseInt(e.target.value) || 0 })} + className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" + /> +
+ +
+ + setFormData({ ...formData, reorder_point: parseInt(e.target.value) || 0 })} + className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" + /> +
+
+ +
+ + + +
+ +
+ +