feat: implement global error handling with reusable dialog and widget for improved user feedback
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -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<String, dynamic> && 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<String, dynamic>)).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;
|
||||
|
||||
@@ -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: <Widget>[
|
||||
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!')),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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<ReceiptImportTab> {
|
||||
}
|
||||
|
||||
Future<void> _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<ReceiptImportTab> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<RecipeImportTab> {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Submit ───────────────────────────────────────────────────────────────
|
||||
// ── Import ────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> _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<RecipeImportTab> {
|
||||
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<RecipeImportTab> {
|
||||
token: token,
|
||||
)
|
||||
: await repo.importUrl(
|
||||
url: _urlCtrl.text.trim(),
|
||||
url: _urlCtrl.text,
|
||||
token: token,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
// Explicitly typed as <String, dynamic> so the router's
|
||||
// 'is Map<String, dynamic>' runtime check succeeds.
|
||||
context.push('/recipes/create', extra: <String, dynamic>{
|
||||
'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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user