feat: add utility functions for date and quantity formatting; refactor inventory and recipe screens to use new formatters
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-04 20:50:18 +02:00
parent a645d6a364
commit 5411dfe2c0
9 changed files with 40 additions and 41 deletions
+23
View File
@@ -0,0 +1,23 @@
/// Formaterar ett DateTime-objekt som YYYY-MM-DD.
/// Om [dt] är null returneras [fallback] (default: tom sträng).
String formatDate(DateTime? dt, {String fallback = ''}) {
if (dt == null) return fallback;
return '${dt.year}-'
'${dt.month.toString().padLeft(2, '0')}-'
'${dt.day.toString().padLeft(2, '0')}';
}
/// Parsar en ISO-datumsträng och formaterar som YYYY-MM-DD.
/// Om strängen inte är parsbar returneras den oförändrad.
String formatDateString(String iso) {
try {
return formatDate(DateTime.parse(iso), fallback: iso);
} catch (_) {
return iso;
}
}
/// Formaterar ett tal som heltal om det inte har decimaler, annars med decimaler.
/// Exempel: 2.0 → "2", 1.5 → "1.5"
String formatQuantity(double v) =>
v == v.roundToDouble() ? v.toStringAsFixed(0) : v.toStringAsFixed(1);
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../../core/api/api_error_mapper.dart'; import '../../../core/api/api_error_mapper.dart';
import '../../../core/utils/formatters.dart';
import '../../../core/api/api_paths.dart'; import '../../../core/api/api_paths.dart';
import '../../../core/api/api_providers.dart'; import '../../../core/api/api_providers.dart';
import '../../../core/forms/form_options.dart'; import '../../../core/forms/form_options.dart';
@@ -142,10 +143,7 @@ class _CreateInventoryScreenState
} }
} }
String _formatDate(DateTime? dt) { String _formatDate(DateTime? dt) => formatDate(dt, fallback: context.l10n.selectDateLabel);
if (dt == null) return context.l10n.selectDateLabel;
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../../core/api/api_error_mapper.dart'; import '../../../core/api/api_error_mapper.dart';
import '../../../core/utils/formatters.dart';
import '../../../core/l10n/l10n.dart'; import '../../../core/l10n/l10n.dart';
import '../../../core/ui/async_state_views.dart'; import '../../../core/ui/async_state_views.dart';
import '../../auth/data/auth_providers.dart'; import '../../auth/data/auth_providers.dart';
@@ -77,14 +78,7 @@ class InventoryDetailScreen extends ConsumerWidget {
); );
} }
String _formatDate(String iso) { String _formatDate(String iso) => formatDateString(iso);
try {
final dt = DateTime.parse(iso);
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
} catch (_) {
return iso;
}
}
} }
class _DeleteButton extends ConsumerWidget { class _DeleteButton extends ConsumerWidget {
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../../core/api/api_error_mapper.dart'; import '../../../core/api/api_error_mapper.dart';
import '../../../core/utils/formatters.dart';
import '../../../core/forms/form_options.dart'; import '../../../core/forms/form_options.dart';
import '../../../core/l10n/l10n.dart'; import '../../../core/l10n/l10n.dart';
import '../../../core/ui/async_state_views.dart'; import '../../../core/ui/async_state_views.dart';
@@ -117,10 +118,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
} }
} }
String _formatDate(DateTime? dt) { String _formatDate(DateTime? dt) => formatDate(dt, fallback: context.l10n.selectDateLabel);
if (dt == null) return context.l10n.selectDateLabel;
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../../core/api/api_error_mapper.dart'; import '../../../core/api/api_error_mapper.dart';
import '../../../core/utils/formatters.dart';
import '../../auth/data/auth_providers.dart'; import '../../auth/data/auth_providers.dart';
import '../data/inventory_providers.dart'; import '../data/inventory_providers.dart';
import '../domain/inventory_item.dart'; import '../domain/inventory_item.dart';
@@ -287,17 +288,9 @@ class _ForegroundTile extends ConsumerWidget {
); );
} }
String _fmtQty(double v) => String _fmtQty(double v) => formatQuantity(v);
v == v.roundToDouble() ? v.toStringAsFixed(0) : v.toStringAsFixed(1);
String _formatDate(String iso) { String _formatDate(String iso) => formatDateString(iso);
try {
final dt = DateTime.parse(iso);
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-'
'${dt.day.toString().padLeft(2, '0')}';
} catch (_) {
return iso;
}
} }
} }
@@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../../../core/api/api_error_mapper.dart'; import '../../../core/api/api_error_mapper.dart';
import '../../../core/utils/formatters.dart';
import '../../../core/l10n/l10n.dart'; import '../../../core/l10n/l10n.dart';
import '../../../core/ui/async_state_views.dart'; import '../../../core/ui/async_state_views.dart';
import '../../auth/data/auth_providers.dart'; import '../../auth/data/auth_providers.dart';
@@ -591,8 +592,5 @@ class _EnrichedShoppingItem {
} }
} }
String _formatQuantity(double value) { String _formatQuantity(double value) => formatQuantity(value);
final normalized = value == value.roundToDouble() ? value.toStringAsFixed(0) : value.toStringAsFixed(1);
return normalized.replaceAll(RegExp(r'\.0$'), '');
}
} }
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import '../../../core/api/api_error_mapper.dart'; import '../../../core/api/api_error_mapper.dart';
import '../../../core/api/api_exception.dart'; import '../../../core/api/api_exception.dart';
import '../../../core/utils/formatters.dart';
import '../../../core/l10n/l10n.dart'; import '../../../core/l10n/l10n.dart';
import '../../auth/data/auth_providers.dart'; import '../../auth/data/auth_providers.dart';
import '../data/recipe_providers.dart'; import '../data/recipe_providers.dart';
@@ -305,11 +306,7 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
} }
Widget _buildIngredientRow(int index, ParsedIngredient ing) { Widget _buildIngredientRow(int index, ParsedIngredient ing) {
final qtyStr = ing.quantity > 0 final qtyStr = ing.quantity > 0 ? '${formatQuantity(ing.quantity)} ' : '';
? (ing.quantity == ing.quantity.truncateToDouble()
? '${ing.quantity.toInt()} '
: '${ing.quantity} ')
: '';
final unitStr = ing.unit.isNotEmpty ? '${ing.unit} ' : ''; final unitStr = ing.unit.isNotEmpty ? '${ing.unit} ' : '';
final noteStr = ing.note != null ? ' (${ing.note})' : ''; final noteStr = ing.note != null ? ' (${ing.note})' : '';
final label = '$qtyStr$unitStr${ing.rawName}$noteStr'; final label = '$qtyStr$unitStr${ing.rawName}$noteStr';
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import '../../../core/api/api_error_mapper.dart'; import '../../../core/api/api_error_mapper.dart';
import '../../../core/api/api_exception.dart'; import '../../../core/api/api_exception.dart';
import '../../../core/utils/formatters.dart';
import '../../../core/auth/jwt_decoder.dart'; import '../../../core/auth/jwt_decoder.dart';
import '../../../core/l10n/l10n.dart'; import '../../../core/l10n/l10n.dart';
import '../../../core/ui/async_state_views.dart'; import '../../../core/ui/async_state_views.dart';
@@ -12,8 +13,7 @@ import '../data/recipe_providers.dart';
import '../domain/recipe.dart'; import '../domain/recipe.dart';
import '../domain/inventory_preview.dart'; import '../domain/inventory_preview.dart';
String _fmtQty(double v) => String _fmtQty(double v) => formatQuantity(v);
v == v.truncateToDouble() ? v.toInt().toString() : v.toString();
enum _ShareAction { share, unshare } enum _ShareAction { share, unshare }
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import '../../../core/api/api_error_mapper.dart'; import '../../../core/api/api_error_mapper.dart';
import '../../../core/api/api_exception.dart'; import '../../../core/api/api_exception.dart';
import '../../../core/utils/formatters.dart';
import '../../../core/api/api_paths.dart'; import '../../../core/api/api_paths.dart';
import '../../../core/api/api_providers.dart'; import '../../../core/api/api_providers.dart';
import '../../../core/forms/form_options.dart'; import '../../../core/forms/form_options.dart';
@@ -31,13 +32,10 @@ class _EditableIngredient {
noteCtrl = TextEditingController(text: note); noteCtrl = TextEditingController(text: note);
factory _EditableIngredient.fromRecipe(RecipeIngredient ingredient) { factory _EditableIngredient.fromRecipe(RecipeIngredient ingredient) {
final quantity = ingredient.quantity == ingredient.quantity.truncateToDouble()
? ingredient.quantity.toInt().toString()
: ingredient.quantity.toString();
return _EditableIngredient( return _EditableIngredient(
productId: ingredient.productId, productId: ingredient.productId,
productName: ingredient.productName, productName: ingredient.productName,
quantity: quantity, quantity: formatQuantity(ingredient.quantity),
unit: ingredient.unit, unit: ingredient.unit,
note: ingredient.note ?? '', note: ingredient.note ?? '',
); );