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,11 +624,11 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
|
||||
{inventoryItems.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{inventoryItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="p-4 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div key={item.id}>
|
||||
{/* Show ingredient card only if NOT editing this item */}
|
||||
{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-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h5 className="font-medium text-[var(--text-primary)]">
|
||||
@@ -828,6 +828,205 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Inline Edit Form - show only when editing this specific ingredient */}
|
||||
{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">
|
||||
<h4 className="font-semibold text-[var(--text-primary)] mb-4">
|
||||
Editar Ingrediente
|
||||
</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"
|
||||
>
|
||||
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>
|
||||
@@ -840,11 +1039,11 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add/Edit Form */}
|
||||
{isAdding ? (
|
||||
{/* 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">
|
||||
{editingId ? 'Editar Ingrediente' : 'Agregar Ingrediente Manualmente'}
|
||||
Agregar Ingrediente Manualmente
|
||||
</h4>
|
||||
<form onSubmit={handleSubmitForm} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -1023,7 +1222,7 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
|
||||
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"
|
||||
>
|
||||
{editingId ? 'Guardar Cambios' : 'Agregar a Lista'}
|
||||
Agregar a Lista
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user