feat: enhance ingredient management; add editable fields for quantity, unit, and notes in recipe creation
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-04 21:43:43 +02:00
parent f32f69db5d
commit 64f63b3392
@@ -52,15 +52,22 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
late Map<int, int?> _selectedProductIds;
late Map<int, String?> _selectedProductNames;
late Map<int, TextEditingController> _qtyControllers;
late Map<int, TextEditingController> _unitControllers;
late Map<int, TextEditingController> _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<CreateRecipeScreen> {
_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<CreateRecipeScreen> {
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<CreateRecipeScreen> {
}
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<int>(
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<int>(
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),
),
),
),
],
),
),
],
),
);
}