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
Test Suite / test (24.15.0) (push) Has been cancelled
This commit is contained in:
@@ -52,15 +52,22 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
late Map<int, int?> _selectedProductIds;
|
late Map<int, int?> _selectedProductIds;
|
||||||
late Map<int, String?> _selectedProductNames;
|
late Map<int, String?> _selectedProductNames;
|
||||||
|
|
||||||
|
late Map<int, TextEditingController> _qtyControllers;
|
||||||
|
late Map<int, TextEditingController> _unitControllers;
|
||||||
|
late Map<int, TextEditingController> _noteControllers;
|
||||||
|
|
||||||
bool _isSaving = false;
|
bool _isSaving = false;
|
||||||
String? _saveError;
|
String? _saveError;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_markdownCtrl.dispose(); // always non-null after initState
|
_markdownCtrl.dispose();
|
||||||
if (_step == _Step.review) {
|
if (_step == _Step.review) {
|
||||||
_nameCtrl.dispose();
|
_nameCtrl.dispose();
|
||||||
_servingsCtrl.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();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -71,11 +78,19 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
_included = List.generate(parsed.ingredients.length, (_) => true);
|
_included = List.generate(parsed.ingredients.length, (_) => true);
|
||||||
_selectedProductIds = {};
|
_selectedProductIds = {};
|
||||||
_selectedProductNames = {};
|
_selectedProductNames = {};
|
||||||
|
_qtyControllers = {};
|
||||||
|
_unitControllers = {};
|
||||||
|
_noteControllers = {};
|
||||||
for (var i = 0; i < parsed.ingredients.length; i++) {
|
for (var i = 0; i < parsed.ingredients.length; i++) {
|
||||||
final suggestions = parsed.ingredients[i].suggestions;
|
final ing = parsed.ingredients[i];
|
||||||
if (suggestions.isNotEmpty) {
|
_qtyControllers[i] = TextEditingController(
|
||||||
_selectedProductIds[i] = suggestions.first.productId;
|
text: ing.quantity > 0 ? formatQuantity(ing.quantity) : '',
|
||||||
_selectedProductNames[i] = suggestions.first.productName;
|
);
|
||||||
|
_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 {
|
} else {
|
||||||
_selectedProductIds[i] = null;
|
_selectedProductIds[i] = null;
|
||||||
_selectedProductNames[i] = null;
|
_selectedProductNames[i] = null;
|
||||||
@@ -128,12 +143,17 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
if (!_included[i]) continue;
|
if (!_included[i]) continue;
|
||||||
final productId = _selectedProductIds[i];
|
final productId = _selectedProductIds[i];
|
||||||
if (productId == null) continue;
|
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({
|
ingredients.add({
|
||||||
'productId': productId,
|
'productId': productId,
|
||||||
'quantity': ing.quantity,
|
'quantity': qty,
|
||||||
'unit': ing.unit,
|
'unit': unit,
|
||||||
if (ing.note != null) 'note': ing.note,
|
if (note.isNotEmpty) 'note': note,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,45 +326,104 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildIngredientRow(int index, ParsedIngredient ing) {
|
Widget _buildIngredientRow(int index, ParsedIngredient ing) {
|
||||||
final qtyStr = ing.quantity > 0 ? '${formatQuantity(ing.quantity)} ' : '';
|
final isIncluded = _included[index];
|
||||||
final unitStr = ing.unit.isNotEmpty ? '${ing.unit} ' : '';
|
final noProductFound = ing.suggestions.isEmpty;
|
||||||
final noteStr = ing.note != null ? ' (${ing.note})' : '';
|
// Problem #2: tydlig varning om rad är inkluderad men saknar produkt
|
||||||
final label = '$qtyStr$unitStr${ing.rawName}$noteStr';
|
final showMissingProductWarning = isIncluded && noProductFound;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child: CheckboxListTile(
|
child: Column(
|
||||||
value: _included[index],
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
onChanged: (v) => setState(() => _included[index] = v ?? false),
|
children: [
|
||||||
title: Text(label),
|
CheckboxListTile(
|
||||||
subtitle: ing.suggestions.isEmpty
|
value: isIncluded,
|
||||||
? Text(
|
onChanged: (v) => setState(() => _included[index] = v ?? false),
|
||||||
context.l10n.recipeCreateNoProductFound,
|
title: Text(ing.rawName),
|
||||||
style: TextStyle(
|
subtitle: noProductFound
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
? Text(
|
||||||
fontSize: 12),
|
context.l10n.recipeCreateNoProductFound,
|
||||||
)
|
style: TextStyle(
|
||||||
: DropdownButton<int>(
|
color: showMissingProductWarning
|
||||||
value: _selectedProductIds[index],
|
? Theme.of(context).colorScheme.error
|
||||||
isExpanded: true,
|
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
onChanged: _included[index]
|
fontSize: 12,
|
||||||
? (id) {
|
),
|
||||||
if (id == null) return;
|
)
|
||||||
setState(() {
|
: DropdownButton<int>(
|
||||||
_selectedProductIds[index] = id;
|
value: _selectedProductIds[index],
|
||||||
_selectedProductNames[index] = ing.suggestions
|
isExpanded: true,
|
||||||
.firstWhere((s) => s.productId == id)
|
onChanged: isIncluded
|
||||||
.productName;
|
? (id) {
|
||||||
});
|
if (id == null) return;
|
||||||
}
|
setState(() {
|
||||||
: null,
|
_selectedProductIds[index] = id;
|
||||||
items: ing.suggestions
|
_selectedProductNames[index] = ing.suggestions
|
||||||
.map((s) => DropdownMenuItem(
|
.firstWhere((s) => s.productId == id)
|
||||||
value: s.productId,
|
.productName;
|
||||||
child: Text(s.productName),
|
});
|
||||||
))
|
}
|
||||||
.toList(),
|
: 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user