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