From 5231ca42a72715a380687b9269014a18c9a00e28 Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Thu, 30 Apr 2026 12:01:47 +0200 Subject: [PATCH] feat: implement global error handling with reusable dialog and widget for improved user feedback Co-authored-by: Copilot --- .../lib/core/utils/global_error_handler.dart | 38 ++++++++++++++++++ .../core/widgets/error_message_widget.dart | 40 +++++++++++++++++++ .../import/data/import_repository.dart | 15 ++++--- .../import/presentation/error_dialog.dart | 38 ++++++++++++++++++ .../presentation/receipt_import_tab.dart | 14 +++++-- .../presentation/recipe_import_tab.dart | 25 ++++++------ 6 files changed, 149 insertions(+), 21 deletions(-) create mode 100644 flutter/lib/core/utils/global_error_handler.dart create mode 100644 flutter/lib/core/widgets/error_message_widget.dart create mode 100644 flutter/lib/features/import/presentation/error_dialog.dart diff --git a/flutter/lib/core/utils/global_error_handler.dart b/flutter/lib/core/utils/global_error_handler.dart new file mode 100644 index 00000000..71b1f0af --- /dev/null +++ b/flutter/lib/core/utils/global_error_handler.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Visar en global dialogruta med ett felmeddelande och en kopieringsknapp. +void showGlobalErrorDialog(BuildContext context, String errorMessage) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Fel'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText(errorMessage), + ], + ), + actions: [ + TextButton( + child: const Text('Stäng'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('Kopiera'), + onPressed: () { + Clipboard.setData(ClipboardData(text: errorMessage)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Felmeddelande kopierat!')), + ); + }, + ), + ], + ); + }, + ); +} \ No newline at end of file diff --git a/flutter/lib/core/widgets/error_message_widget.dart b/flutter/lib/core/widgets/error_message_widget.dart new file mode 100644 index 00000000..60aa1ec2 --- /dev/null +++ b/flutter/lib/core/widgets/error_message_widget.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// En återanvändbar widget för att visa felmeddelanden med en kopieringsknapp. +class ErrorMessageWidget extends StatelessWidget { + final String errorMessage; + + const ErrorMessageWidget({Key? key, required this.errorMessage}) : super(key: key); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Fel'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText(errorMessage), + ], + ), + actions: [ + TextButton( + child: const Text('Stäng'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('Kopiera'), + onPressed: () { + Clipboard.setData(ClipboardData(text: errorMessage)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Felmeddelande kopierat!')), + ); + }, + ), + ], + ); + } +} \ No newline at end of file diff --git a/flutter/lib/features/import/data/import_repository.dart b/flutter/lib/features/import/data/import_repository.dart index 904e12dd..a498bd4f 100644 --- a/flutter/lib/features/import/data/import_repository.dart +++ b/flutter/lib/features/import/data/import_repository.dart @@ -71,15 +71,18 @@ class ImportRepository { } final parsed = _parseResponse(response); - final items = (parsed['items'] as List?) ?? parsed as List?; - if (items == null) { - developer.log('Invalid response format: ${response.body}', name: 'ImportRepository', error: 'Invalid Data'); - throw ApiException(type: ApiErrorType.unknown, message: 'Felaktigt svar från servern.'); + // Check if the response is a ReceiptImportResult + if (parsed is Map && parsed.containsKey('items')) { + final items = (parsed['items'] as List?)?.map((e) => ParsedReceiptItem.fromJson(e)).toList(); + if (items != null) { + developer.log('Successfully parsed ${items.length} items', name: 'ImportRepository'); + return items; + } } - developer.log('Successfully parsed ${items.length} items', name: 'ImportRepository'); - return items.map((e) => ParsedReceiptItem.fromJson(e as Map)).toList(); + developer.log('Invalid response format: ${response.body}', name: 'ImportRepository', error: 'Invalid Data'); + throw ApiException(type: ApiErrorType.unknown, message: 'Felaktigt svar från servern.'); } catch (e) { developer.log('Exception during receipt import: $e', name: 'ImportRepository', error: e); rethrow; diff --git a/flutter/lib/features/import/presentation/error_dialog.dart b/flutter/lib/features/import/presentation/error_dialog.dart new file mode 100644 index 00000000..78af5a24 --- /dev/null +++ b/flutter/lib/features/import/presentation/error_dialog.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Visar en dialogruta med ett felmeddelande och en kopieringsknapp. +void showErrorDialog(BuildContext context, String errorMessage) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Fel'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText(errorMessage), + ], + ), + actions: [ + TextButton( + child: const Text('Stäng'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('Kopiera'), + onPressed: () { + Clipboard.setData(ClipboardData(text: errorMessage)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Felmeddelande kopierat!')), + ); + }, + ), + ], + ); + }, + ); +} \ No newline at end of file diff --git a/flutter/lib/features/import/presentation/receipt_import_tab.dart b/flutter/lib/features/import/presentation/receipt_import_tab.dart index c970921a..f5e5e3f3 100644 --- a/flutter/lib/features/import/presentation/receipt_import_tab.dart +++ b/flutter/lib/features/import/presentation/receipt_import_tab.dart @@ -2,6 +2,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/api/api_error_mapper.dart'; +import '../../../core/utils/global_error_handler.dart'; import '../../auth/data/auth_providers.dart'; import '../data/import_providers.dart'; import '../domain/parsed_receipt_item.dart'; @@ -34,11 +35,17 @@ class _ReceiptImportTabState extends ConsumerState { } Future _submit() async { + if (_pickedFile == null) { + setState(() => _error = 'Vänligen välj en fil först'); + return; + } + setState(() { _isLoading = true; _error = null; _items = null; }); + try { final token = await ref.read(authStateProvider.future); final repo = ref.read(importRepositoryProvider); @@ -50,10 +57,11 @@ class _ReceiptImportTabState extends ConsumerState { if (!mounted) return; setState(() => _items = items); } catch (e) { - if (!mounted) return; - setState(() => _error = mapErrorToUserMessage(e, context)); + showGlobalErrorDialog(context, 'Ett fel uppstod vid import: $e'); } finally { - if (mounted) setState(() => _isLoading = false); + if (mounted) { + setState(() => _isLoading = false); + } } } diff --git a/flutter/lib/features/import/presentation/recipe_import_tab.dart b/flutter/lib/features/import/presentation/recipe_import_tab.dart index b47d2800..c1d2e909 100644 --- a/flutter/lib/features/import/presentation/recipe_import_tab.dart +++ b/flutter/lib/features/import/presentation/recipe_import_tab.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/api/api_error_mapper.dart'; +import '../../../core/utils/global_error_handler.dart'; import '../../auth/data/auth_providers.dart'; import '../data/import_providers.dart'; @@ -54,9 +55,14 @@ class _RecipeImportTabState extends ConsumerState { }); } - // ── Submit ─────────────────────────────────────────────────────────────── + // ── Import ──────────────────────────────────────────────────────────── Future _submit() async { + if (_pickedFile == null && _method == _Method.file) { + setState(() => _error = 'Vänligen välj en fil först'); + return; + } + setState(() { _isLoading = true; _error = null; @@ -65,7 +71,6 @@ class _RecipeImportTabState extends ConsumerState { try { final token = await ref.read(authStateProvider.future); final repo = ref.read(importRepositoryProvider); - final result = _method == _Method.file ? await repo.importFile( bytes: _pickedFile!.bytes!, @@ -73,22 +78,18 @@ class _RecipeImportTabState extends ConsumerState { token: token, ) : await repo.importUrl( - url: _urlCtrl.text.trim(), + url: _urlCtrl.text, token: token, ); if (!mounted) return; - // Explicitly typed as so the router's - // 'is Map' runtime check succeeds. - context.push('/recipes/create', extra: { - 'markdown': result.markdown, - 'imageUrl': result.imageUrl, - }).then((_) => context.go('/recipes')); + context.push('/recipes/create', extra: result); } catch (e) { - if (!mounted) return; - setState(() => _error = mapErrorToUserMessage(e, context)); + showGlobalErrorDialog(context, 'Ett fel uppstod vid import: $e'); } finally { - if (mounted) setState(() => _isLoading = false); + if (mounted) { + setState(() => _isLoading = false); + } } }