feat(receipt-import): enhance package handling with new fields and UI adjustments

This commit is contained in:
Nils-Johan Gynther
2026-05-02 10:11:31 +02:00
parent f9dc55d42c
commit 9161de5d52
4 changed files with 251 additions and 78 deletions
+8 -7
View File
@@ -74,13 +74,14 @@
## Nästa steg ## Nästa steg
1. Inför hybrid alias-modell för kvittoimport: user-scope alias som standard + global alias som admin-verifierad fallback. 1. Kvittoimport steg 2: persistenta förpackningsfält i inventarie (packCount, packSizeQuantity, packSizeUnit) + visning/redigering i inventory-UI.
2. Uppdatera backend-matchordning för alias: user-alias -> global alias -> poängbaserat namnförslag -> AI-kategori. 2. Inför hybrid alias-modell för kvittoimport: user-scope alias som standard + global alias som admin-verifierad fallback.
3. Implementera automatisk alias-inlärning vid manuell korrigering i importflödet (först user-scope). 3. Uppdatera backend-matchordning för alias: user-alias -> global alias -> poängbaserat namnförslag -> AI-kategori.
4. Deploy och smoke-test av kvittoimportflödet på server. 4. Implementera automatisk alias-inlärning vid manuell korrigering i importflödet (först user-scope).
5. Fortsatt flytt av UI-strängar till ARB (inventarie, pantry, recept). 5. Deploy och smoke-test av kvittoimportflödet på server.
6. Smoke-test på testdomän och avstämning. 6. Fortsatt flytt av UI-strängar till ARB (inventarie, pantry, recept).
7. Planera och påbörja avancerad AI-integration och EAN-skanning. 7. Smoke-test på testdomän och avstämning.
8. Planera och påbörja avancerad AI-integration och EAN-skanning.
## Beslut 2026-05-02 - Aliasstrategi för kvittoimport ## Beslut 2026-05-02 - Aliasstrategi för kvittoimport
@@ -423,26 +423,30 @@ class _UserTile extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(user.email, style: theme.textTheme.bodySmall), Text(user.email, style: theme.textTheme.bodySmall),
Row( Wrap(
spacing: 4,
runSpacing: 2,
children: [ children: [
Chip( ActionChip(
label: Text(user.role), label: Text(user.isAdmin ? 'Admin' : 'User'),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
labelStyle: theme.textTheme.labelSmall, labelStyle: theme.textTheme.labelSmall,
tooltip: user.isAdmin ? 'Nedgradera till user' : 'Uppgradera till admin',
onPressed: onChangeRole,
), ),
if (user.isPremium) ...[ ActionChip(
const SizedBox(width: 4), label: Text(user.isPremium ? 'Premium' : 'Free'),
Chip( padding: EdgeInsets.zero,
label: const Text('Premium'), visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero, labelStyle: theme.textTheme.labelSmall,
visualDensity: VisualDensity.compact, backgroundColor: user.isPremium
labelStyle: theme.textTheme.labelSmall, ? theme.colorScheme.tertiaryContainer
backgroundColor: theme.colorScheme.tertiaryContainer, : theme.colorScheme.surfaceContainerHighest,
), tooltip: user.isPremium ? 'Ta bort Premium' : 'Ge Premium',
], onPressed: onTogglePremium,
const SizedBox(width: 4), ),
Chip( ActionChip(
label: Text(user.canShareRecipes ? 'Delning: På' : 'Delning: Av'), label: Text(user.canShareRecipes ? 'Delning: På' : 'Delning: Av'),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
@@ -450,6 +454,8 @@ class _UserTile extends StatelessWidget {
backgroundColor: user.canShareRecipes backgroundColor: user.canShareRecipes
? theme.colorScheme.secondaryContainer ? theme.colorScheme.secondaryContainer
: theme.colorScheme.errorContainer, : theme.colorScheme.errorContainer,
tooltip: user.canShareRecipes ? 'Blockera receptdelning' : 'Tillåt receptdelning',
onPressed: onToggleRecipeSharing,
), ),
], ],
), ),
@@ -18,6 +18,9 @@ class ItemEdit {
final CategorySelectionSource? categorySource; final CategorySelectionSource? categorySource;
final double? quantity; final double? quantity;
final String? unit; final String? unit;
final double? packQuantity;
final String? packUnit;
final double? packageCount;
final ImportDestination destination; final ImportDestination destination;
const ItemEdit({ const ItemEdit({
@@ -28,6 +31,9 @@ class ItemEdit {
this.categorySource, this.categorySource,
this.quantity, this.quantity,
this.unit, this.unit,
this.packQuantity,
this.packUnit,
this.packageCount,
this.destination = ImportDestination.inventory, this.destination = ImportDestination.inventory,
}); });
} }
@@ -22,14 +22,8 @@ typedef _Destination = ImportDestination;
enum _ProductEntryMode { existing, create } enum _ProductEntryMode { existing, create }
({double quantity, String unit})? _normalizePackageQuantityFromRawName({ bool _isPackageLikeUnit(String? unit) {
required String rawName, if (unit == null) return false;
required double? quantity,
required String? unit,
}) {
if (quantity == null || unit == null) return null;
final normalizedUnit = unit.trim().toLowerCase();
const packageUnits = { const packageUnits = {
'paket', 'paket',
'forpackning', 'forpackning',
@@ -41,37 +35,63 @@ enum _ProductEntryMode { existing, create }
'fp', 'fp',
'pkt', 'pkt',
'pack', 'pack',
'pak',
'st', 'st',
'styck', 'styck',
}; };
if (!packageUnits.contains(normalizedUnit)) return null; return packageUnits.contains(unit.trim().toLowerCase());
}
({double packQuantity, String packUnit})? _extractPackageSizeFromRawName(
String rawName,
) {
final match = RegExp( final match = RegExp(
r'(\d+(?:[\.,]\d+)?)\s*(ml|cl|dl|l|g|kg)\b', r'(\d+(?:[\.,]\d+)?)\s*(ml|cl|dl|l|g|kg)\b',
caseSensitive: false, caseSensitive: false,
).firstMatch(rawName); ).firstMatch(rawName);
if (match == null) return null; if (match == null) return null;
final value = double.tryParse(match.group(1)!.replaceAll(',', '.')); final value = double.tryParse(match.group(1)!.replaceAll(',', '.'));
final sizeUnit = match.group(2)!.toLowerCase(); final sizeUnit = match.group(2)!.toLowerCase();
if (value == null) return null; if (value == null) return null;
return (packQuantity: value, packUnit: sizeUnit);
}
switch (sizeUnit) { ({double? packQuantity, String? packUnit, double packageCount, double? totalQuantity, String? totalUnit})
case 'ml': _inferPackageFields({
return (quantity: quantity * (value / 1000.0), unit: 'l'); required String rawName,
case 'cl': required double? quantity,
return (quantity: quantity * (value / 100.0), unit: 'l'); required String? unit,
case 'dl': }) {
return (quantity: quantity * (value / 10.0), unit: 'l'); if (quantity == null || unit == null) {
case 'l': return (
return (quantity: quantity * value, unit: 'l'); packQuantity: null,
case 'g': packUnit: null,
return (quantity: quantity * (value / 1000.0), unit: 'kg'); packageCount: 1,
case 'kg': totalQuantity: quantity,
return (quantity: quantity * value, unit: 'kg'); totalUnit: unit,
default: );
return null;
} }
final looksLikePackage = _isPackageLikeUnit(unit);
final extracted = _extractPackageSizeFromRawName(rawName);
if (looksLikePackage && extracted != null) {
return (
packQuantity: extracted.packQuantity,
packUnit: extracted.packUnit,
packageCount: quantity,
totalQuantity: extracted.packQuantity * quantity,
totalUnit: extracted.packUnit,
);
}
return (
packQuantity: quantity,
packUnit: unit,
packageCount: 1,
totalQuantity: quantity,
totalUnit: unit,
);
} }
String _formatCompactNumber(double value) { String _formatCompactNumber(double value) {
@@ -86,6 +106,48 @@ String _formatSwedishNumber(double value) {
return _formatCompactNumber(value).replaceAll('.', ','); return _formatCompactNumber(value).replaceAll('.', ',');
} }
double? _convertQuantity(double quantity, String fromUnit, String toUnit) {
final from = fromUnit.trim().toLowerCase();
final to = toUnit.trim().toLowerCase();
if (from.isEmpty || to.isEmpty) return null;
if (from == to) return quantity;
// Mass
if (from == 'mg' && to == 'g') return quantity / 1000.0;
if (from == 'mg' && to == 'kg') return quantity / 1000000.0;
if (from == 'mg' && to == 'hg') return quantity / 100000.0;
if (from == 'g' && to == 'mg') return quantity * 1000.0;
if (from == 'g' && to == 'hg') return quantity / 100.0;
if (from == 'g' && to == 'kg') return quantity / 1000.0;
if (from == 'hg' && to == 'mg') return quantity * 100000.0;
if (from == 'hg' && to == 'g') return quantity * 100.0;
if (from == 'hg' && to == 'kg') return quantity / 10.0;
if (from == 'kg' && to == 'mg') return quantity * 1000000.0;
if (from == 'kg' && to == 'hg') return quantity * 10.0;
if (from == 'kg' && to == 'g') return quantity * 1000.0;
// Volume
if (from == 'ml' && to == 'l') return quantity / 1000.0;
if (from == 'cl' && to == 'l') return quantity / 100.0;
if (from == 'dl' && to == 'l') return quantity / 10.0;
if (from == 'l' && to == 'ml') return quantity * 1000.0;
if (from == 'l' && to == 'cl') return quantity * 100.0;
if (from == 'l' && to == 'dl') return quantity * 10.0;
// Intra-volume conversions
if (from == 'ml' && to == 'cl') return quantity / 10.0;
if (from == 'ml' && to == 'dl') return quantity / 100.0;
if (from == 'cl' && to == 'ml') return quantity * 10.0;
if (from == 'cl' && to == 'dl') return quantity / 10.0;
if (from == 'dl' && to == 'ml') return quantity * 100.0;
if (from == 'dl' && to == 'cl') return quantity * 10.0;
return null;
}
/// Konverterar VERSALER-produktnamn till Title Case med smarta regler: /// Konverterar VERSALER-produktnamn till Title Case med smarta regler:
/// - Token med `/` (förkortningar) lämnas i versaler: KY/KAL/LE/TO /// - Token med `/` (förkortningar) lämnas i versaler: KY/KAL/LE/TO
/// - Token som börjar med siffra (mängd/storlek) görs till gemener: 284g, 12x85g /// - Token som börjar med siffra (mängd/storlek) görs till gemener: 284g, 12x85g
@@ -129,6 +191,7 @@ class _EditDialog extends StatefulWidget {
class _EditDialogState extends State<_EditDialog> { class _EditDialogState extends State<_EditDialog> {
late final TextEditingController _quantityCtrl; late final TextEditingController _quantityCtrl;
late final TextEditingController _unitCtrl; late final TextEditingController _unitCtrl;
late final TextEditingController _packageCountCtrl;
late final TextEditingController _newProductNameCtrl; late final TextEditingController _newProductNameCtrl;
int? _productId; int? _productId;
String? _productName; String? _productName;
@@ -161,11 +224,23 @@ class _EditDialogState extends State<_EditDialog> {
_newCategoryId = widget.current.categoryId ?? widget.item.categorySuggestionId; _newCategoryId = widget.current.categoryId ?? widget.item.categorySuggestionId;
_newCategoryPath = widget.current.categoryPath ?? widget.item.categorySuggestionPath; _newCategoryPath = widget.current.categoryPath ?? widget.item.categorySuggestionPath;
_newCategorySource = widget.current.categorySource; _newCategorySource = widget.current.categorySource;
final inferred = _inferPackageFields(
rawName: widget.item.rawName,
quantity: widget.current.quantity ?? widget.item.quantity,
unit: widget.current.unit ?? widget.item.unit,
);
final initialPackQuantity = widget.current.packQuantity ?? inferred.packQuantity;
final initialPackUnit = widget.current.packUnit ?? inferred.packUnit;
final initialPackageCount = widget.current.packageCount ?? inferred.packageCount;
_quantityCtrl = TextEditingController( _quantityCtrl = TextEditingController(
text: (widget.current.quantity ?? widget.item.quantity)?.toString() ?? '', text: initialPackQuantity?.toString() ?? '',
); );
_unitCtrl = TextEditingController( _unitCtrl = TextEditingController(
text: widget.current.unit ?? widget.item.unit ?? '', text: initialPackUnit ?? '',
);
_packageCountCtrl = TextEditingController(
text: initialPackageCount.toString(),
); );
_newProductNameCtrl = TextEditingController( _newProductNameCtrl = TextEditingController(
text: _normalizeProductName(widget.current.productName ?? widget.item.rawName), text: _normalizeProductName(widget.current.productName ?? widget.item.rawName),
@@ -200,6 +275,7 @@ class _EditDialogState extends State<_EditDialog> {
void dispose() { void dispose() {
_quantityCtrl.dispose(); _quantityCtrl.dispose();
_unitCtrl.dispose(); _unitCtrl.dispose();
_packageCountCtrl.dispose();
_newProductNameCtrl.dispose(); _newProductNameCtrl.dispose();
super.dispose(); super.dispose();
} }
@@ -309,6 +385,14 @@ class _EditDialogState extends State<_EditDialog> {
if (!mounted || _productId == null) return; if (!mounted || _productId == null) return;
} }
final packQuantity = double.tryParse(_quantityCtrl.text.replaceAll(',', '.'));
final packageCount =
double.tryParse(_packageCountCtrl.text.replaceAll(',', '.')) ?? 1.0;
final packUnit = _unitCtrl.text.trim().isEmpty
? (widget.current.packUnit ?? widget.current.unit ?? widget.item.unit)
: _unitCtrl.text.trim();
final totalQuantity = packQuantity != null ? packQuantity * packageCount : widget.item.quantity;
Navigator.pop( Navigator.pop(
context, context,
_ItemEdit( _ItemEdit(
@@ -317,8 +401,11 @@ class _EditDialogState extends State<_EditDialog> {
categoryId: _productCategoryId, categoryId: _productCategoryId,
categoryPath: _productCategoryPath, categoryPath: _productCategoryPath,
categorySource: _productCategorySource, categorySource: _productCategorySource,
quantity: double.tryParse(_quantityCtrl.text) ?? widget.item.quantity, quantity: totalQuantity,
unit: _unitCtrl.text.trim().isEmpty ? widget.item.unit : _unitCtrl.text.trim(), unit: packUnit,
packQuantity: packQuantity,
packUnit: packUnit,
packageCount: packageCount,
destination: _destination, destination: _destination,
), ),
); );
@@ -338,16 +425,15 @@ class _EditDialogState extends State<_EditDialog> {
: (item.matchedProductName?.isNotEmpty == true : (item.matchedProductName?.isNotEmpty == true
? _normalizeProductName(item.matchedProductName!) ? _normalizeProductName(item.matchedProductName!)
: null); : null);
final currentQuantity = final currentPackQuantity =
double.tryParse(_quantityCtrl.text.replaceAll(',', '.')) ?? widget.item.quantity; double.tryParse(_quantityCtrl.text.replaceAll(',', '.'));
final currentUnit = _unitCtrl.text.trim().isEmpty ? widget.item.unit : _unitCtrl.text.trim(); final currentPackageCount =
final normalizationPreview = _destination == _Destination.inventory double.tryParse(_packageCountCtrl.text.replaceAll(',', '.')) ?? 1.0;
? _normalizePackageQuantityFromRawName( final currentUnit = _unitCtrl.text.trim().isEmpty
rawName: item.rawName, ? (widget.current.packUnit ?? widget.current.unit ?? widget.item.unit)
quantity: currentQuantity, : _unitCtrl.text.trim();
unit: currentUnit, final totalPreview =
) currentPackQuantity == null ? null : currentPackQuantity * currentPackageCount;
: null;
return AlertDialog( return AlertDialog(
title: Text( title: Text(
@@ -592,7 +678,10 @@ class _EditDialogState extends State<_EditDialog> {
child: TextField( child: TextField(
controller: _quantityCtrl, controller: _quantityCtrl,
keyboardType: const TextInputType.numberWithOptions(decimal: true), keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(labelText: 'Antal', border: OutlineInputBorder()), decoration: const InputDecoration(
labelText: 'Mangd per forpackning',
border: OutlineInputBorder(),
),
onChanged: (_) => setState(() {}), onChanged: (_) => setState(() {}),
), ),
), ),
@@ -600,12 +689,25 @@ class _EditDialogState extends State<_EditDialog> {
Expanded( Expanded(
child: TextField( child: TextField(
controller: _unitCtrl, controller: _unitCtrl,
decoration: const InputDecoration(labelText: 'Enhet', border: OutlineInputBorder()), decoration: const InputDecoration(
labelText: 'Enhet',
border: OutlineInputBorder(),
),
onChanged: (_) => setState(() {}), onChanged: (_) => setState(() {}),
), ),
), ),
]), ]),
if (normalizationPreview != null) ...[ const SizedBox(height: 8),
TextField(
controller: _packageCountCtrl,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(
labelText: 'Antal forpackningar',
border: OutlineInputBorder(),
),
onChanged: (_) => setState(() {}),
),
if (totalPreview != null && currentUnit != null && currentUnit.isNotEmpty) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Container( Container(
width: double.infinity, width: double.infinity,
@@ -616,8 +718,8 @@ class _EditDialogState extends State<_EditDialog> {
border: Border.all(color: Colors.green.shade200), border: Border.all(color: Colors.green.shade200),
), ),
child: Text( child: Text(
'Tolkat som totalt ${_formatSwedishNumber(normalizationPreview.quantity)} ${normalizationPreview.unit} ' 'Totalt: ${_formatSwedishNumber(totalPreview)} $currentUnit '
'(antal x förpackningsstorlek).', '(mangd x antal forpackningar).',
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: Colors.green.shade800, color: Colors.green.shade800,
), ),
@@ -821,6 +923,11 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
final pid = it.matchedProductId ?? it.suggestedProductId; final pid = it.matchedProductId ?? it.suggestedProductId;
notifier.setSelected(i, pid != null); notifier.setSelected(i, pid != null);
if (pid != null) { if (pid != null) {
final inferred = _inferPackageFields(
rawName: it.rawName,
quantity: it.quantity,
unit: it.unit,
);
final name = it.matchedProductName ?? it.suggestedProductName; final name = it.matchedProductName ?? it.suggestedProductName;
final resolvedCategoryId = it.categorySuggestionId ?? _categoryIdForProduct(pid); final resolvedCategoryId = it.categorySuggestionId ?? _categoryIdForProduct(pid);
final resolvedCategoryPath = it.categorySuggestionPath ?? final resolvedCategoryPath = it.categorySuggestionPath ??
@@ -833,8 +940,11 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
categorySource: it.categorySuggestionId != null categorySource: it.categorySuggestionId != null
? CategorySelectionSource.ai ? CategorySelectionSource.ai
: null, : null,
quantity: it.quantity, quantity: inferred.totalQuantity,
unit: it.unit, unit: inferred.totalUnit,
packQuantity: inferred.packQuantity,
packUnit: inferred.packUnit,
packageCount: inferred.packageCount,
)); ));
} }
} }
@@ -852,6 +962,11 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
_ProductEntryMode? initialEntryMode, _ProductEntryMode? initialEntryMode,
}) async { }) async {
final item = _items![index]; final item = _items![index];
final inferred = _inferPackageFields(
rawName: item.rawName,
quantity: item.quantity,
unit: item.unit,
);
final current = _edits[index] ?? final current = _edits[index] ??
_ItemEdit( _ItemEdit(
productId: item.matchedProductId ?? item.suggestedProductId, productId: item.matchedProductId ?? item.suggestedProductId,
@@ -866,8 +981,11 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
categorySource: item.categorySuggestionId != null categorySource: item.categorySuggestionId != null
? CategorySelectionSource.ai ? CategorySelectionSource.ai
: null, : null,
quantity: item.quantity, quantity: inferred.totalQuantity,
unit: item.unit, unit: inferred.totalUnit,
packQuantity: inferred.packQuantity,
packUnit: inferred.packUnit,
packageCount: inferred.packageCount,
); );
final result = await showDialog<_ItemEdit>( final result = await showDialog<_ItemEdit>(
@@ -948,18 +1066,26 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
pantryAdded++; pantryAdded++;
} }
} else { } else {
final normalized = _normalizePackageQuantityFromRawName( final inferred = _inferPackageFields(
rawName: item.rawName, rawName: item.rawName,
quantity: edit.quantity ?? item.quantity, quantity: edit.quantity ?? item.quantity,
unit: edit.unit ?? item.unit, unit: edit.unit ?? item.unit,
); );
final qty = normalized?.quantity ?? edit.quantity ?? item.quantity ?? 1.0; final packageCount = edit.packageCount ?? inferred.packageCount;
final unit = normalized?.unit ?? edit.unit ?? item.unit ?? 'st'; final packQuantity = edit.packQuantity ?? inferred.packQuantity;
final packUnit = edit.packUnit ?? inferred.packUnit ?? edit.unit ?? item.unit ?? 'st';
final qty = packQuantity != null
? (packQuantity * packageCount)
: (edit.quantity ?? inferred.totalQuantity ?? item.quantity ?? 1.0);
final unit = packUnit;
final existing = _inventoryByProduct[pid]; final existing = _inventoryByProduct[pid];
if (existing != null) { final qtyInExistingUnit = existing == null
? null
: _convertQuantity(qty, unit, existing.unit);
if (existing != null && qtyInExistingUnit != null) {
await invRepo.updateInventoryItem( await invRepo.updateInventoryItem(
existing.id, existing.id,
{'quantity': existing.quantity + qty}, {'quantity': existing.quantity + qtyInExistingUnit},
token: token, token: token,
); );
merged++; merged++;
@@ -1129,9 +1255,32 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
final hasProduct = edit?.productId != null; final hasProduct = edit?.productId != null;
final isMatched = item.matchedProductId != null; final isMatched = item.matchedProductId != null;
final isSuggested = item.suggestedProductId != null && item.matchedProductId == null; final isSuggested = item.suggestedProductId != null && item.matchedProductId == null;
final existingInv = edit?.productId != null && edit?.destination != _Destination.pantry final existingInv = edit?.productId != null && edit?.destination != _Destination.pantry
? _inventoryByProduct[edit!.productId] ? _inventoryByProduct[edit!.productId]
: null; : null;
final inferredForPreview = _inferPackageFields(
rawName: item.rawName,
quantity: edit?.quantity ?? item.quantity,
unit: edit?.unit ?? item.unit,
);
final previewPackageCount = edit?.packageCount ?? inferredForPreview.packageCount;
final previewPackQuantity = edit?.packQuantity ?? inferredForPreview.packQuantity;
final previewIncomingQty = previewPackQuantity != null
? (previewPackQuantity * previewPackageCount)
: (edit?.quantity ?? inferredForPreview.totalQuantity ?? item.quantity ?? 0);
final previewIncomingUnit = edit?.packUnit ??
inferredForPreview.packUnit ??
edit?.unit ??
item.unit ??
'st';
final convertedPreviewQty = existingInv == null
? null
: _convertQuantity(
previewIncomingQty,
previewIncomingUnit,
existingInv.unit,
);
final canMergePreview = existingInv != null && convertedPreviewQty != null;
final alreadyInPantry = edit?.productId != null && edit?.destination == _Destination.pantry final alreadyInPantry = edit?.productId != null && edit?.destination == _Destination.pantry
? _pantryProductIds.contains(edit!.productId) ? _pantryProductIds.contains(edit!.productId)
: false; : false;
@@ -1250,17 +1399,28 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
], ],
), ),
], ],
if (existingInv != null) ...[ if (existingInv != null && canMergePreview) ...[
const SizedBox(height: 2), const SizedBox(height: 2),
Row(children: [ Row(children: [
Icon(Icons.kitchen_outlined, size: 12, color: Colors.blue.shade700), Icon(Icons.kitchen_outlined, size: 12, color: Colors.blue.shade700),
const SizedBox(width: 3), const SizedBox(width: 3),
Text( Text(
'I lager: ${existingInv.quantity} ${existingInv.unit} → blir ${(existingInv.quantity + (edit?.quantity ?? item.quantity ?? 0)).toStringAsFixed(existingInv.quantity % 1 == 0 ? 0 : 2)} ${existingInv.unit}', 'I lager: ${existingInv.quantity} ${existingInv.unit} → blir ${(existingInv.quantity + (convertedPreviewQty ?? 0)).toStringAsFixed(existingInv.quantity % 1 == 0 ? 0 : 2)} ${existingInv.unit}',
style: theme.textTheme.bodySmall?.copyWith(color: Colors.blue.shade700), style: theme.textTheme.bodySmall?.copyWith(color: Colors.blue.shade700),
), ),
]), ]),
], ],
if (existingInv != null && !canMergePreview) ...[
const SizedBox(height: 2),
Row(children: [
Icon(Icons.info_outline, size: 12, color: Colors.orange.shade700),
const SizedBox(width: 3),
Text(
'Finns i lager med annan enhet (${existingInv.unit}) - sparas som ny rad',
style: theme.textTheme.bodySmall?.copyWith(color: Colors.orange.shade700),
),
]),
],
if (alreadyInPantry) ...[ if (alreadyInPantry) ...[
const SizedBox(height: 2), const SizedBox(height: 2),
Row(children: [ Row(children: [