Fix inline edit form positioning in AI inventory configuration
**Issue:** When clicking "Edit" on an ingredient in the list, the edit form
appeared at the bottom of the page after all ingredients, forcing users to
scroll down. This was poor UX especially with 10+ ingredients.
**Solution:** Moved edit form to appear inline directly below the ingredient
being edited.
**Changes Made:**
1. **Inline Edit Form Display**
- Edit form now renders inside the ingredient map loop
- Shows conditionally when `editingId === item.id`
- Appears immediately below the specific ingredient being edited
- Location: frontend/src/components/domain/onboarding/steps/UploadSalesDataStep.tsx:834-1029
2. **Hide Ingredient Card While Editing**
- Ingredient card (with stock lots) hidden when that ingredient is being edited
- Condition: `{editingId !== item.id && (...)}`
- Prevents duplication of information
- Location: lines 629-832
3. **Separate Add Manually Form**
- Bottom form now only shows when adding new ingredient (not editing)
- Condition changed from `{isAdding ? (` to `{isAdding && !editingId ? (`
- Title simplified to "Agregar Ingrediente Manualmente"
- Button label simplified to "Agregar a Lista"
- Location: lines 1042-1237
**User Experience:**
Before: Edit button → scroll to bottom → edit form → scroll back up
After: Edit button → form appears right there → edit → save → continues
**Structure:**
```jsx
{inventoryItems.map((item) => (
<div key={item.id}>
{editingId !== item.id && (
<>
{/* Ingredient card */}
{/* Stock lots section */}
</>
)}
{editingId === item.id && (
{/* Inline edit form */}
)}
</div>
))}
{isAdding && !editingId && (
{/* Add manually form at bottom */}
)}
```
**Build Status:** ✓ Successful in 20.61s
This commit is contained in:
@@ -624,10 +624,10 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
|
|||||||
{inventoryItems.length > 0 ? (
|
{inventoryItems.length > 0 ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{inventoryItems.map((item) => (
|
{inventoryItems.map((item) => (
|
||||||
<div
|
<div key={item.id}>
|
||||||
key={item.id}
|
{/* Show ingredient card only if NOT editing this item */}
|
||||||
className="p-4 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg"
|
{editingId !== item.id && (
|
||||||
>
|
<div className="p-4 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -829,22 +829,13 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
|
|||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8 border border-dashed border-[var(--border-secondary)] rounded-lg">
|
|
||||||
<p className="text-[var(--text-tertiary)] text-sm">
|
|
||||||
No hay ingredientes en la lista todavía
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Add/Edit Form */}
|
{/* Inline Edit Form - show only when editing this specific ingredient */}
|
||||||
{isAdding ? (
|
{editingId === item.id && (
|
||||||
<div className="border-2 border-[var(--color-primary)] rounded-lg p-4 bg-gradient-to-br from-[var(--color-primary)]/5 to-transparent">
|
<div className="border-2 border-[var(--color-primary)] rounded-lg p-4 bg-gradient-to-br from-[var(--color-primary)]/5 to-transparent">
|
||||||
<h4 className="font-semibold text-[var(--text-primary)] mb-4">
|
<h4 className="font-semibold text-[var(--text-primary)] mb-4">
|
||||||
{editingId ? 'Editar Ingrediente' : 'Agregar Ingrediente Manualmente'}
|
Editar Ingrediente
|
||||||
</h4>
|
</h4>
|
||||||
<form onSubmit={handleSubmitForm} className="space-y-4">
|
<form onSubmit={handleSubmitForm} className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
@@ -1023,7 +1014,215 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] transition-colors font-medium"
|
className="px-4 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] transition-colors font-medium"
|
||||||
>
|
>
|
||||||
{editingId ? 'Guardar Cambios' : 'Agregar a Lista'}
|
Guardar Cambios
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={resetForm}
|
||||||
|
className="px-4 py-2 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)] rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 border border-dashed border-[var(--border-secondary)] rounded-lg">
|
||||||
|
<p className="text-[var(--text-tertiary)] text-sm">
|
||||||
|
No hay ingredientes en la lista todavía
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Manually Form - only show when adding new (not editing existing) */}
|
||||||
|
{isAdding && !editingId ? (
|
||||||
|
<div className="border-2 border-[var(--color-primary)] rounded-lg p-4 bg-gradient-to-br from-[var(--color-primary)]/5 to-transparent">
|
||||||
|
<h4 className="font-semibold text-[var(--text-primary)] mb-4">
|
||||||
|
Agregar Ingrediente Manualmente
|
||||||
|
</h4>
|
||||||
|
<form onSubmit={handleSubmitForm} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Nombre *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => 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 && (
|
||||||
|
<p className="text-xs text-[var(--color-error)] mt-1">{formErrors.name}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Categoría *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formData.category}
|
||||||
|
onChange={(e) => setFormData({ ...formData, category: 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)]"
|
||||||
|
>
|
||||||
|
<option value="">Seleccionar...</option>
|
||||||
|
{categoryOptions.map(cat => (
|
||||||
|
<option key={cat} value={cat}>{cat}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{formErrors.category && (
|
||||||
|
<p className="text-xs text-[var(--color-error)] mt-1">{formErrors.category}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Unidad de Medida
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formData.unit_of_measure}
|
||||||
|
onChange={(e) => setFormData({ ...formData, unit_of_measure: 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)]"
|
||||||
|
>
|
||||||
|
{unitOptions.map(unit => (
|
||||||
|
<option key={unit} value={unit}>{unit}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Stock Inicial
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
value={formData.stock_quantity}
|
||||||
|
onChange={(e) => 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 && (
|
||||||
|
<p className="text-xs text-[var(--color-error)] mt-1">{formErrors.stock_quantity}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Costo por Unidad (€)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
value={formData.cost_per_unit}
|
||||||
|
onChange={(e) => 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 && (
|
||||||
|
<p className="text-xs text-[var(--color-error)] mt-1">{formErrors.cost_per_unit}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Días de Caducidad
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={formData.estimated_shelf_life_days}
|
||||||
|
onChange={(e) => 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 && (
|
||||||
|
<p className="text-xs text-[var(--color-error)] mt-1">{formErrors.estimated_shelf_life_days}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Stock Mínimo
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={formData.low_stock_threshold}
|
||||||
|
onChange={(e) => 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)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Punto de Reorden
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={formData.reorder_point}
|
||||||
|
onChange={(e) => 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)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<label className="flex items-center gap-2 text-sm text-[var(--text-primary)]">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.requires_refrigeration}
|
||||||
|
onChange={(e) => setFormData({ ...formData, requires_refrigeration: e.target.checked })}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
Requiere Refrigeración
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 text-sm text-[var(--text-primary)]">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.requires_freezing}
|
||||||
|
onChange={(e) => setFormData({ ...formData, requires_freezing: e.target.checked })}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
Requiere Congelación
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 text-sm text-[var(--text-primary)]">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.is_seasonal}
|
||||||
|
onChange={(e) => setFormData({ ...formData, is_seasonal: e.target.checked })}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
Estacional
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||||
|
Notas (opcional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={formData.notes}
|
||||||
|
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
|
||||||
|
rows={2}
|
||||||
|
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="Información adicional..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Agregar a Lista
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user