feat: add location field to PantryItem model and update related functionality
Test Suite / test (24.15.0) (push) Has been cancelled

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Nils-Johan Gynther
2026-05-06 11:54:56 +02:00
parent 63d249b0a8
commit e7251fd94c
8 changed files with 114 additions and 37 deletions
@@ -10,7 +10,9 @@ import '../../../core/forms/form_options.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/ui/product_picker_field.dart';
import '../../auth/data/auth_providers.dart';
import '../../pantry/data/pantry_providers.dart';
import '../data/inventory_providers.dart';
import '../../import/data/receipt_import_session.dart' show ImportDestination;
class CreateInventoryScreen extends ConsumerStatefulWidget {
const CreateInventoryScreen({super.key});
@@ -36,6 +38,7 @@ class _CreateInventoryScreenState
DateTime? _bestBeforeDate;
bool _opened = false;
bool _saving = false;
ImportDestination _destination = ImportDestination.inventory;
@override
void initState() {
@@ -101,13 +104,40 @@ class _CreateInventoryScreenState
}
Future<void> _save() async {
if (!_formKey.currentState!.validate()) return;
if (_selectedProductId == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.inventorySelectProduct)),
);
return;
}
if (_destination == ImportDestination.pantry) {
setState(() => _saving = true);
try {
final token = await ref.read(authStateProvider.future);
await ref
.read(pantryRepositoryProvider)
.createPantryItem(
_selectedProductId!,
token: token,
location: _locationController.text.trim().isEmpty
? null
: _locationController.text.trim(),
);
ref.invalidate(pantryProvider);
if (mounted) context.pop();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)));
}
} finally {
if (mounted) setState(() => _saving = false);
}
return;
}
if (!_formKey.currentState!.validate()) return;
setState(() => _saving = true);
try {
final token = await ref.read(authStateProvider.future);
@@ -163,6 +193,8 @@ class _CreateInventoryScreenState
)
.toList();
final isInventory = _destination == ImportDestination.inventory;
return Scaffold(
appBar: AppBar(title: Text(context.l10n.inventoryCreateTitle)),
body: Form(
@@ -170,6 +202,25 @@ class _CreateInventoryScreenState
child: ListView(
padding: const EdgeInsets.all(16),
children: [
SegmentedButton<ImportDestination>(
segments: const [
ButtonSegment(
value: ImportDestination.inventory,
label: Text('Inventarie'),
icon: Icon(Icons.inventory_2_outlined),
),
ButtonSegment(
value: ImportDestination.pantry,
label: Text('Skafferi'),
icon: Icon(Icons.kitchen_outlined),
),
],
selected: {_destination},
onSelectionChanged: _saving
? null
: (selected) => setState(() => _destination = selected.first),
),
const SizedBox(height: 16),
ProductPickerField(
products: productOptions,
value: _selectedProductId,
@@ -179,7 +230,31 @@ class _CreateInventoryScreenState
onChanged: (value) => setState(() => _selectedProductId = value),
),
const SizedBox(height: 12),
Row(
DropdownButtonFormField<String>(
initialValue: _locationController.text.trim().isEmpty
? null
: _locationController.text.trim(),
isExpanded: true,
decoration: InputDecoration(
labelText: context.l10n.locationOptionalLabel,
border: const OutlineInputBorder(),
),
items: inventoryLocationOptions
.map(
(location) => DropdownMenuItem<String>(
value: location,
child: Text(location),
),
)
.toList(),
onChanged: _saving
? null
: (value) => setState(() {
_locationController.text = value ?? '';
}),
),
if (isInventory) const SizedBox(height: 12),
if (isInventory) Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
@@ -232,32 +307,8 @@ class _CreateInventoryScreenState
),
],
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: _locationController.text.trim().isEmpty
? null
: _locationController.text.trim(),
isExpanded: true,
decoration: InputDecoration(
labelText: context.l10n.locationOptionalLabel,
border: const OutlineInputBorder(),
),
items: inventoryLocationOptions
.map(
(location) => DropdownMenuItem<String>(
value: location,
child: Text(location),
),
)
.toList(),
onChanged: _saving
? null
: (value) => setState(() {
_locationController.text = value ?? '';
}),
),
const SizedBox(height: 12),
TextFormField(
if (isInventory) const SizedBox(height: 12),
if (isInventory) TextFormField(
controller: _brandController,
decoration: InputDecoration(
labelText: context.l10n.brandOptionalLabel,
@@ -265,8 +316,8 @@ class _CreateInventoryScreenState
),
enabled: !_saving,
),
const SizedBox(height: 12),
Row(
if (isInventory) const SizedBox(height: 12),
if (isInventory) Row(
children: [
Expanded(
child: OutlinedButton.icon(
@@ -291,7 +342,7 @@ class _CreateInventoryScreenState
),
],
),
CheckboxListTile(
if (isInventory) CheckboxListTile(
title: Text(context.l10n.openedLabel),
value: _opened,
onChanged:
@@ -299,7 +350,7 @@ class _CreateInventoryScreenState
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
),
TextFormField(
if (isInventory) TextFormField(
controller: _commentController,
decoration: InputDecoration(
labelText: context.l10n.commentOptionalLabel,
@@ -27,7 +27,7 @@ class PantryRepository {
Future<List<PantryProduct>> fetchProducts({String? token}) async {
try {
final data = await _api.getJson(ProductApiPaths.list, token: token);
final data = await _api.getJson(ProductApiPaths.mine, token: token);
final list = data as List<dynamic>;
_logger.info('Fetched ${list.length} products');
return list
@@ -39,11 +39,19 @@ class PantryRepository {
}
}
Future<PantryItem> createPantryItem(int productId, {String? token}) async {
Future<PantryItem> createPantryItem(
int productId, {
String? token,
String? location,
}) async {
try {
final data = await _api.postJson(
PantryApiPaths.list,
body: {'productId': productId},
body: {
'productId': productId,
if (location != null && location.trim().isNotEmpty)
'location': location.trim(),
},
token: token,
);
_logger.info('Created pantry item for product ID: $productId');
@@ -5,6 +5,7 @@ class PantryItem {
final String? canonicalName;
final String? category;
final int? categoryId;
final String? location;
const PantryItem({
required this.id,
@@ -13,6 +14,7 @@ class PantryItem {
this.canonicalName,
this.category,
this.categoryId,
this.location,
});
String get displayName {
@@ -31,6 +33,7 @@ class PantryItem {
canonicalName: product['canonicalName']?.toString(),
category: product['category']?.toString(),
categoryId: (product['categoryId'] as num?)?.toInt(),
location: json['location']?.toString(),
);
}
}
@@ -374,6 +374,9 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text(item.displayName),
subtitle: item.location == null || item.location!.trim().isEmpty
? null
: Text('Plats: ${item.location}'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [