import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/api/api_error_mapper.dart'; import '../../../core/api/api_exception.dart'; import '../../auth/data/auth_providers.dart'; import '../data/recipe_providers.dart'; import '../domain/parsed_recipe.dart'; enum _Step { input, review } class CreateRecipeScreen extends ConsumerStatefulWidget { /// Optional markdown to pre-fill the input field, e.g. from import. final String? initialMarkdown; /// Optional image URL pre-filled from a quick-import result. final String? initialImageUrl; const CreateRecipeScreen({ super.key, this.initialMarkdown, this.initialImageUrl, }); @override ConsumerState createState() => _CreateRecipeScreenState(); } class _CreateRecipeScreenState extends ConsumerState { _Step _step = _Step.input; // Step 1 — markdown input late final TextEditingController _markdownCtrl; bool _isParsing = false; String? _parseError; @override void initState() { super.initState(); _markdownCtrl = TextEditingController(text: widget.initialMarkdown ?? ''); } // Step 2 — review state ParsedRecipe? _parsed; late TextEditingController _nameCtrl; late TextEditingController _servingsCtrl; late List _included; late Map _selectedProductIds; late Map _selectedProductNames; bool _isSaving = false; String? _saveError; @override void dispose() { _markdownCtrl.dispose(); // always non-null after initState if (_step == _Step.review) { _nameCtrl.dispose(); _servingsCtrl.dispose(); } super.dispose(); } void _initReviewState(ParsedRecipe parsed) { _nameCtrl = TextEditingController(text: parsed.name); _servingsCtrl = TextEditingController(); _included = List.generate(parsed.ingredients.length, (_) => true); _selectedProductIds = {}; _selectedProductNames = {}; 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; } else { _selectedProductIds[i] = null; _selectedProductNames[i] = null; } } } Future _parseMarkdown() async { final markdown = _markdownCtrl.text.trim(); if (markdown.isEmpty) { setState(() => _parseError = 'Klistra in eller skriv ett recept i Markdown-format.'); return; } setState(() { _isParsing = true; _parseError = null; }); try { final token = await ref.read(authStateProvider.future); final parsed = await ref .read(recipeRepositoryProvider) .parseMarkdown(markdown, token: token); _initReviewState(parsed); setState(() { _parsed = parsed; _step = _Step.review; _isParsing = false; }); } on ApiException catch (e) { if (e.type == ApiErrorType.unauthorized) { await ref.read(authStateProvider.notifier).logout(); return; } setState(() { _parseError = mapErrorToUserMessage(e, context); _isParsing = false; }); } } Future _save() async { final name = _nameCtrl.text.trim(); if (name.isEmpty) { setState(() => _saveError = 'Receptnamnet får inte vara tomt.'); return; } final ingredients = >[]; for (var i = 0; i < _parsed!.ingredients.length; i++) { if (!_included[i]) continue; final productId = _selectedProductIds[i]; if (productId == null) continue; final ing = _parsed!.ingredients[i]; ingredients.add({ 'productId': productId, 'quantity': ing.quantity, 'unit': ing.unit, if (ing.note != null) 'note': ing.note, }); } setState(() { _isSaving = true; _saveError = null; }); try { final token = await ref.read(authStateProvider.future); final servingsRaw = int.tryParse(_servingsCtrl.text.trim()); final created = await ref.read(recipeRepositoryProvider).createRecipe( { 'name': name, if (_parsed!.description != null) 'description': _parsed!.description, if (servingsRaw != null) 'servings': servingsRaw, if (_parsed!.instructions != null) 'instructions': _parsed!.instructions, if (widget.initialImageUrl != null && widget.initialImageUrl!.isNotEmpty) 'imageUrl': widget.initialImageUrl, 'ingredients': ingredients, }, token: token, ); ref.invalidate(recipesProvider); if (mounted) context.go('/recipes/${created.id}'); } on ApiException catch (e) { if (e.type == ApiErrorType.unauthorized) { await ref.read(authStateProvider.notifier).logout(); return; } setState(() { _saveError = mapErrorToUserMessage(e, context); _isSaving = false; }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(_step == _Step.input ? 'Nytt recept' : 'Granska ingredienser'), leading: _step == _Step.review ? IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => setState(() { _step = _Step.input; _nameCtrl.dispose(); _servingsCtrl.dispose(); }), ) : null, ), body: _step == _Step.input ? _buildInputStep() : _buildReviewStep(), ); } Widget _buildInputStep() { return Column( children: [ Expanded( child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: TextField( controller: _markdownCtrl, maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, decoration: const InputDecoration( hintText: '# Receptnamn\n\n## Ingredienser\n- 500 g köttfärs\n- 1 st lök\n\n## Tillvägagångssätt\nStek löken...', border: OutlineInputBorder(), alignLabelWithHint: true, ), ), ), ), if (_parseError != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( _parseError!, style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), Padding( padding: const EdgeInsets.all(16), child: SizedBox( width: double.infinity, child: FilledButton( onPressed: _isParsing ? null : _parseMarkdown, child: _isParsing ? const SizedBox( height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Granska ingredienser'), ), ), ), ], ); } Widget _buildReviewStep() { final parsed = _parsed!; return Column( children: [ Expanded( child: ListView( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), children: [ TextField( controller: _nameCtrl, decoration: const InputDecoration(labelText: 'Receptnamn'), ), const SizedBox(height: 12), TextField( controller: _servingsCtrl, decoration: const InputDecoration( labelText: 'Antal portioner (valfritt)'), keyboardType: TextInputType.number, ), if (parsed.ingredients.isNotEmpty) ...[ const SizedBox(height: 20), Text('Ingredienser', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 4), Text( 'Bocka av ingredienser att inkludera och välj rätt produkt.', style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 8), ...List.generate( parsed.ingredients.length, (i) => _buildIngredientRow(i, parsed.ingredients[i])), ], const SizedBox(height: 8), ], ), ), if (_saveError != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( _saveError!, style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), Padding( padding: const EdgeInsets.all(16), child: SizedBox( width: double.infinity, child: FilledButton( onPressed: _isSaving ? null : _save, child: _isSaving ? const SizedBox( height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Spara recept'), ), ), ), ], ); } Widget _buildIngredientRow(int index, ParsedIngredient ing) { final qtyStr = ing.quantity > 0 ? (ing.quantity == ing.quantity.truncateToDouble() ? '${ing.quantity.toInt()} ' : '${ing.quantity} ') : ''; final unitStr = ing.unit.isNotEmpty ? '${ing.unit} ' : ''; final noteStr = ing.note != null ? ' (${ing.note})' : ''; final label = '$qtyStr$unitStr${ing.rawName}$noteStr'; 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( 'Ingen produkt hittades — ingrediensen hoppas över.', 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(), ), ), ); } }