From 64f63b3392fd7f5c06280641d43552a2c41a83a4 Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Mon, 4 May 2026 21:43:43 +0200 Subject: [PATCH] feat: enhance ingredient management; add editable fields for quantity, unit, and notes in recipe creation --- .../presentation/create_recipe_screen.dart | 167 +++++++++++++----- 1 file changed, 123 insertions(+), 44 deletions(-) diff --git a/flutter/lib/features/recipes/presentation/create_recipe_screen.dart b/flutter/lib/features/recipes/presentation/create_recipe_screen.dart index 966b318a..ca6f36c7 100644 --- a/flutter/lib/features/recipes/presentation/create_recipe_screen.dart +++ b/flutter/lib/features/recipes/presentation/create_recipe_screen.dart @@ -52,15 +52,22 @@ class _CreateRecipeScreenState extends ConsumerState { late Map _selectedProductIds; late Map _selectedProductNames; + late Map _qtyControllers; + late Map _unitControllers; + late Map _noteControllers; + bool _isSaving = false; String? _saveError; @override void dispose() { - _markdownCtrl.dispose(); // always non-null after initState + _markdownCtrl.dispose(); if (_step == _Step.review) { _nameCtrl.dispose(); _servingsCtrl.dispose(); + for (final c in _qtyControllers.values) c.dispose(); + for (final c in _unitControllers.values) c.dispose(); + for (final c in _noteControllers.values) c.dispose(); } super.dispose(); } @@ -71,11 +78,19 @@ class _CreateRecipeScreenState extends ConsumerState { _included = List.generate(parsed.ingredients.length, (_) => true); _selectedProductIds = {}; _selectedProductNames = {}; + _qtyControllers = {}; + _unitControllers = {}; + _noteControllers = {}; for (var i = 0; i < parsed.ingredients.length; i++) { - final suggestions = parsed.ingredients[i].suggestions; - if (suggestions.isNotEmpty) { - _selectedProductIds[i] = suggestions.first.productId; - _selectedProductNames[i] = suggestions.first.productName; + final ing = parsed.ingredients[i]; + _qtyControllers[i] = TextEditingController( + text: ing.quantity > 0 ? formatQuantity(ing.quantity) : '', + ); + _unitControllers[i] = TextEditingController(text: ing.unit); + _noteControllers[i] = TextEditingController(text: ing.note ?? ''); + if (ing.suggestions.isNotEmpty) { + _selectedProductIds[i] = ing.suggestions.first.productId; + _selectedProductNames[i] = ing.suggestions.first.productName; } else { _selectedProductIds[i] = null; _selectedProductNames[i] = null; @@ -128,12 +143,17 @@ class _CreateRecipeScreenState extends ConsumerState { if (!_included[i]) continue; final productId = _selectedProductIds[i]; if (productId == null) continue; - final ing = _parsed!.ingredients[i]; + final qty = double.tryParse( + _qtyControllers[i]!.text.trim().replaceAll(',', '.'), + ) ?? + _parsed!.ingredients[i].quantity; + final unit = _unitControllers[i]!.text.trim(); + final note = _noteControllers[i]!.text.trim(); ingredients.add({ 'productId': productId, - 'quantity': ing.quantity, - 'unit': ing.unit, - if (ing.note != null) 'note': ing.note, + 'quantity': qty, + 'unit': unit, + if (note.isNotEmpty) 'note': note, }); } @@ -306,45 +326,104 @@ class _CreateRecipeScreenState extends ConsumerState { } Widget _buildIngredientRow(int index, ParsedIngredient ing) { - final qtyStr = ing.quantity > 0 ? '${formatQuantity(ing.quantity)} ' : ''; - final unitStr = ing.unit.isNotEmpty ? '${ing.unit} ' : ''; - final noteStr = ing.note != null ? ' (${ing.note})' : ''; - final label = '$qtyStr$unitStr${ing.rawName}$noteStr'; + final isIncluded = _included[index]; + final noProductFound = ing.suggestions.isEmpty; + // Problem #2: tydlig varning om rad är inkluderad men saknar produkt + final showMissingProductWarning = isIncluded && noProductFound; return Card( margin: const EdgeInsets.symmetric(vertical: 4), - child: CheckboxListTile( - value: _included[index], - onChanged: (v) => setState(() => _included[index] = v ?? false), - title: Text(label), - subtitle: ing.suggestions.isEmpty - ? Text( - context.l10n.recipeCreateNoProductFound, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 12), - ) - : DropdownButton( - value: _selectedProductIds[index], - isExpanded: true, - onChanged: _included[index] - ? (id) { - if (id == null) return; - setState(() { - _selectedProductIds[index] = id; - _selectedProductNames[index] = ing.suggestions - .firstWhere((s) => s.productId == id) - .productName; - }); - } - : null, - items: ing.suggestions - .map((s) => DropdownMenuItem( - value: s.productId, - child: Text(s.productName), - )) - .toList(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CheckboxListTile( + value: isIncluded, + onChanged: (v) => setState(() => _included[index] = v ?? false), + title: Text(ing.rawName), + subtitle: noProductFound + ? Text( + context.l10n.recipeCreateNoProductFound, + style: TextStyle( + color: showMissingProductWarning + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12, + ), + ) + : DropdownButton( + value: _selectedProductIds[index], + isExpanded: true, + onChanged: isIncluded + ? (id) { + if (id == null) return; + setState(() { + _selectedProductIds[index] = id; + _selectedProductNames[index] = ing.suggestions + .firstWhere((s) => s.productId == id) + .productName; + }); + } + : null, + items: ing.suggestions + .map((s) => DropdownMenuItem( + value: s.productId, + child: Text(s.productName), + )) + .toList(), + ), + ), + // Problem #1: editerbara qty/unit/note-fält per ingrediens + if (isIncluded) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: Row( + children: [ + SizedBox( + width: 72, + child: TextField( + controller: _qtyControllers[index], + decoration: const InputDecoration( + labelText: 'Mängd', + isDense: true, + border: OutlineInputBorder(), + contentPadding: + EdgeInsets.symmetric(horizontal: 8, vertical: 8), + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true), + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 72, + child: TextField( + controller: _unitControllers[index], + decoration: const InputDecoration( + labelText: 'Enhet', + isDense: true, + border: OutlineInputBorder(), + contentPadding: + EdgeInsets.symmetric(horizontal: 8, vertical: 8), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: _noteControllers[index], + decoration: const InputDecoration( + labelText: 'Not', + isDense: true, + border: OutlineInputBorder(), + contentPadding: + EdgeInsets.symmetric(horizontal: 8, vertical: 8), + ), + ), + ), + ], ), + ), + ], ), ); }