Files
recipe-app/flutter/lib/features/recipes/presentation/recipe_edit_screen.dart
T

205 lines
6.7 KiB
Dart

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 '../../../core/ui/async_state_views.dart';
import '../../auth/data/auth_providers.dart';
import '../data/recipe_providers.dart';
import '../domain/recipe.dart';
class RecipeEditScreen extends ConsumerStatefulWidget {
final int recipeId;
const RecipeEditScreen({super.key, required this.recipeId});
@override
ConsumerState<RecipeEditScreen> createState() => _RecipeEditScreenState();
}
class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
final _formKey = GlobalKey<FormState>();
bool _initialized = false;
late TextEditingController _nameCtrl;
late TextEditingController _descCtrl;
late TextEditingController _servingsCtrl;
late TextEditingController _instructionsCtrl;
bool _isSaving = false;
String? _saveError;
@override
void dispose() {
if (_initialized) {
_nameCtrl.dispose();
_descCtrl.dispose();
_servingsCtrl.dispose();
_instructionsCtrl.dispose();
}
super.dispose();
}
void _initControllers(Recipe recipe) {
_nameCtrl = TextEditingController(text: recipe.title);
_descCtrl = TextEditingController(text: recipe.description ?? '');
_servingsCtrl =
TextEditingController(text: recipe.servings?.toString() ?? '');
_instructionsCtrl =
TextEditingController(text: recipe.instructions ?? '');
_initialized = true;
}
Future<void> _save() async {
if (!(_formKey.currentState?.validate() ?? false)) return;
setState(() {
_isSaving = true;
_saveError = null;
});
try {
final token = await ref.read(authStateProvider.future);
final servings = int.tryParse(_servingsCtrl.text.trim());
await ref.read(recipeRepositoryProvider).updateRecipe(
widget.recipeId,
{
'name': _nameCtrl.text.trim(),
'description': _descCtrl.text.trim().isEmpty
? null
: _descCtrl.text.trim(),
if (servings != null) 'servings': servings,
'instructions': _instructionsCtrl.text.trim().isEmpty
? null
: _instructionsCtrl.text.trim(),
},
token: token,
);
ref.invalidate(recipeDetailProvider(widget.recipeId));
ref.invalidate(recipesProvider);
if (mounted) context.pop();
} on ApiException catch (e) {
if (e.type == ApiErrorType.unauthorized) {
await ref.read(authStateProvider.notifier).logout();
return;
}
setState(() {
_saveError = mapErrorToUserMessage(e);
_isSaving = false;
});
}
}
@override
Widget build(BuildContext context) {
final recipeAsync = ref.watch(recipeDetailProvider(widget.recipeId));
return Scaffold(
appBar: AppBar(
title: const Text('Redigera recept'),
actions: [
if (_initialized)
TextButton(
onPressed: _isSaving ? null : _save,
child: _isSaving
? const SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Spara'),
),
],
),
body: recipeAsync.when(
loading: () => const LoadingStateView(label: 'Laddar recept...'),
error: (error, _) => ErrorStateView(
message: mapErrorToUserMessage(error),
onRetry: () => ref.invalidate(recipeDetailProvider(widget.recipeId)),
),
data: (recipe) {
if (!_initialized) _initControllers(recipe);
return _buildForm(context);
},
),
);
}
Widget _buildForm(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _nameCtrl,
decoration: const InputDecoration(labelText: 'Receptnamn'),
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'Ange ett receptnamn.' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _descCtrl,
decoration:
const InputDecoration(labelText: 'Beskrivning (valfritt)'),
maxLines: 3,
),
const SizedBox(height: 16),
TextFormField(
controller: _servingsCtrl,
decoration:
const InputDecoration(labelText: 'Antal portioner (valfritt)'),
keyboardType: TextInputType.number,
validator: (v) {
if (v == null || v.trim().isEmpty) return null;
if (int.tryParse(v.trim()) == null) {
return 'Ange ett heltal.';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _instructionsCtrl,
decoration:
const InputDecoration(labelText: 'Tillvägagångssätt (valfritt)'),
maxLines: 10,
textAlignVertical: TextAlignVertical.top,
),
const SizedBox(height: 8),
Text(
'Ingredienser redigeras via Skapa recept-flödet.',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
if (_saveError != null) ...[
const SizedBox(height: 16),
Text(
_saveError!,
style:
TextStyle(color: Theme.of(context).colorScheme.error),
textAlign: TextAlign.center,
),
],
const SizedBox(height: 24),
FilledButton(
onPressed: _isSaving ? null : _save,
child: _isSaving
? const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Spara ändringar'),
),
const SizedBox(height: 40),
],
),
),
);
}
}